diff --git a/docs/search_indexes.json b/docs/search_indexes.json index 20c15b3..f9a2403 100644 --- a/docs/search_indexes.json +++ b/docs/search_indexes.json @@ -1 +1 @@ -[{"URL":"index.html","Title":"Introduction","Body":"QOR5 is a Go library designed to help developers build web applications with ease and high customization. By focusing on static typing in the Go language and minimizing the need for JavaScript or TypeScript, QOR5 streamlines the development process and encourages reusability of components. In QOR5, the traditional approach of using template languages for rendering HTML is discouraged. Instead, QOR5 encourages developers to write HTML using static typing in the Go language . This design choice provides several benefits: Improved Readability and Maintainability: By using Go's static typing, you can maintain a consistent coding style throughout the entire project, making it easier to read and maintain. Better Error Checking: Static typing allows for compile-time error checking, which can help catch issues before they cause problems in production. Enhanced Reusability: QOR5 promotes the use of components, which can be easily abstracted and reused across different parts of your application. Since components are written in Go, using third-party components from other Go packages is as simple as importing and using regular Go packages. Simplified Development Process: By minimizing the need for JavaScript or TypeScript, QOR5 streamlines the development process and reduces the complexity of building interactive web applications. QOR5's approach to rendering HTML using Go's static typing eliminates the need for developers to learn and work with multiple template languages. This results in a more consistent and streamlined development experience, allowing developers to focus on the core functionality of their web applications. How is this document organized 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. Quick Sample Project: We will begin with a brief overview of a sample project, giving you a visual idea of QOR5's capabilities and functionalities. Basic Functions: In this section, we will explore the core features of QOR5, starting from listing pages to editing pages. This section covers common features found in admin websites. QOR5 Essentials and Advanced Functions: We will dive into the inner workings of QOR5, covering topics such as rendering pages and advanced features like partial page refreshing. Digging Deeper: In the final section, you will learn how to create new components for QOR5, extending its capabilities and adapting it to your specific needs. Join the Discord community : https://discord.gg/76YPsVBE4E "},{"URL":"getting-started/one-minute-quick-start.html","Title":"1 Minute Quick Start","Body":"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 : Install the command line tool with: $ go install github.com/qor5/docs/cmd/qor5@latest And run: $ qor5 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\nUse a new terminal to run source dev_env \u0026\u0026 go run main.go to start the admin app "},{"URL":"basics/listing.html","Title":"Listing","Body":"By the 1 Minute Quick Start , 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. 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\") Modify the display value To display the category name rather than category id in the post listing page. The ComponentFunc would do the work.\nThe 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. 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(\u0026c).Error; err != nil { // ignore err in the example } return h.Td(h.Text(c.Name)) }) Display virtual fields 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\")) }) DefaultScope If we want to display Post with disabled=false only. Use the Listing().Searcher to apply SQL conditions. postModelBuilder.Listing().Searcher = func(model interface{}, params *presets.SearchParams, ctx *web.EventContext) (r interface{}, totalCount int, err error){ qdb := db.Where(\"disabled != true\") return gorm2op.DataOperator(qdb).Search(model, params, ctx) } 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() rmn := postModelBuilder.Listing().RowMenu() rmn.RowMenuItem(\"Show\").ComponentFunc(func(obj interface{}, id string, ctx *web.EventContext) h.HTMLComponent { return h.Text(\"Fake Show\") }) Full Example type Post struct { ID uint Title string Body string UpdatedAt time.Time CreatedAt time.Time Disabled bool Status string 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(\u0026Post{}) 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(\"disabled != true\") 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 v.VListItem( v.VListItemIcon(v.VIcon(\"menu\")), v.VListItemTitle(h.Text(\"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(\u0026categories).Error; err != nil { // ignore err for now } return v.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(\u0026c).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(\u0026Category{}) // Use m to customize the model, Or config more models here. return } Check the demo | Source on GitHub "},{"URL":"basics/listing-customizations.html","Title":"Listing Customizations","Body":"We get a default listing page with default columns, But default columns from database\ncolumns rarely fit the needs for any real application. Change List Columns and Component of Field Here is how do we change the columns of the list and how to we change the content display of a columns. 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(\u0026comp, \"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, \u0026web.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(\u0026Company{}) comp.Editing().ValidateFunc(func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) { c := obj.(*Company) if len(c.Name) \u003c 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(\u0026comps) 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\" Check the demo | Source on GitHub 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 Filters Panel Here we continue to add filters for the list 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(\u0026Company{}).Select(\"name as text, id as value\").Scan(\u0026companyOptions).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\" Check the demo | Source on GitHub FilterDataFunc of presets.ListingBuilder setup to have the filter menu or not.\nAnd how it will combine the sql conditions when doing query. the filter menu will\nchange the url query strings with the filter values, and for date type in url query\nstring it uses unix epoch int value. So the sql condition has to convert the database\ncolumn 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 Filter Tabs Filter tabs is based on Filters configuration. But display as tabs above the list,\nYou can think it as a short cut that used very frequently to filter something instead of\nuse the pop up panel of filter. 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(\u0026c) 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\" Check the demo | Source on GitHub Query string name must be from the Filter's item configuration key field. Bulk Actions Bulk actions makes the list row show checkboxes, and you can select one or many rows,\nLater do an bulk update data for all of them. Here is how to use it: 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) \u003c 10 { ctx.Flash = \"comment should larger than 10\" return } err = db.Model(\u0026Customer{}). 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(\u0026Customer{}).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\" Check the demo | Source on GitHub 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 Search Func SearchFunc defines a data processing function for ListingBuilder .\nThis function searches for a model based on the specified search parameters.\nIt returns the search results along with the total count of matching records.\nYou can process the data displayed on the listing page here based on context or custom conditions before pagination. In the following example, the listing page only displays approved customers. func PresetsListingCustomizationSearcher(b *presets.Builder) { db := setupDB() b.URIPrefix(PresetsListingCustomizationSearcherPath).DataOperator(gorm2op.DataOperator(db)) mb := b.Model(\u0026Customer{}) mb.Listing().SearchFunc(func(model interface{}, params *presets.SearchParams, ctx *web.EventContext) (r interface{}, totalCount int, err error) { // only display approved customers qdb := db.Where(\"approved_at IS NOT NULL\") return gorm2op.DataOperator(qdb).Search(model, params, ctx) }) } Check the demo | Source on GitHub "},{"URL":"basics/filter.html","Title":"Filters","Body":"Assume we have a status filed in Post. It has 2 possible values, \"draft\" and \"online\". If we want to filter posts by its status. We can add a filter like this: 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 postBuilder := b.Model(\u0026Post{}) // get its ListingBuilder listing := postBuilder.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\"}, {Text: \"Online\", Value: \"online\"}, } return []*vuetifyx.FilterItem{ { Key: \"status\", Label: \"Status\", ItemType: vuetifyx.ItemTypeSelect, // %s is the condition. e.g. \u003e, \u003e=, =, \u003c, \u003c=, like, // ? is the value of selected option SQLCondition: `status %s ?`, Options: options, }, } }) } Check the demo | Source on GitHub QOR5 now supports 7 types of filter option. PLEASE NOTE THAT all below sample are required you to provide the SQLCondition you want to perform. 1. Filter by String Set the ItemType as vuetifyx.ItemTypeString . No Options needed.\nUnder this mode, the filter would work in 2 ways, the target value equal to the input string the target value contains the input string 2. Filter by Number Set the ItemType as vuetifyx.ItemTypeNumber . No Options needed.\nUnder this mode, the filter would work in 4 ways the target value equal to the input number the target value is between the input numbers the target value is greater than the input number the target value is less than the input number 3. Filter by Date Set the ItemType as vuetifyx.ItemTypeDate . No Options needed.\nUnder this mode, the filter would render a date picker for users to select. 4. Filter by Date Range Set the ItemType as vuetifyx.ItemTypeDateRange . No Options needed.\nUnder this mode, the filter would render 2 date pickers, \"from\" and \"to\" for users to select. 5. Filter by Datetime Range Set the ItemType as vuetifyx.ItemTypeDatetimeRange . No Options needed.\nUnder this mode, the filter would render 2 date time pickers, \"from\" and \"to\" for users to select. 6. Filter by Selectable Items Set the ItemType as vuetifyx.ItemTypeSelect . You need to provide Options like this. The Text is the text users can see in the selector, the Value is the value of the selector. Options: []*vuetifyx.SelectItem{ {Text: \"Active\", Value: \"active\"}, {Text: \"Inactive\", Value: \"inactive\"}, }, 7. Filter by Multiple Select Set the ItemType as vuetifyx.ItemTypeMultipleSelect . You need to provide Options like above \"Selectable Items\". But in this mode, the filter would render the options as multi-selectable checkboxes and the query of this filter becomes IN and NOT IN . "},{"URL":"presets-guide/editing-customizations.html","Title":"Editing","Body":"Editing an object will be always in a drawer popup. select which fields can edit for each model\nby using the .Only func of EditingBuilder , There are different ways to configure the type\nof component that is used to do the editing. Configure field for a single model Use a customized component is as simple as add the extra asset to the preset instance.\nAnd configure the component func on the field: 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\" Check the demo | Source on GitHub 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 Configure field type for all models Set a global field type to component func like the following: 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(\u0026Product{}) 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)) \u003e 0 { img = h.Img(string(val)) } var er h.HTMLComponent if len(field.Errors) \u003e 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(\u0026Product{}) mb.Editing(\"Title\", \"MainImage\") return } const PresetsEditingCustomizationFileTypePath = \"/samples/presets-editing-customization-file-type\" Check the demo | Source on GitHub 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,\nthen 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\nof how to display the edit controls, and How to save the object. Tabs Tabs can be added by using AppendTabsPanelFunc func on EditingBuilder : func PresetsEditingCustomizationTabs(b *presets.Builder) { db := setupDB() b.URIPrefix(PresetsEditingCustomizationTabsPath).DataOperator(gorm2op.DataOperator(db)) mb := b.Model(\u0026Company{}) mb.Listing(\"ID\", \"Name\") mb.Editing().AppendTabsPanelFunc(func(obj interface{}, ctx *web.EventContext) h.HTMLComponent { c := obj.(*Company) return h.Components( v.VTab(h.Text(\"New Tab\")), v.VTabItem( v.VListItemTitle(h.Text(fmt.Sprintf(\"Name: %s\", c.Name))), ).Class(\"pa-4\"), ) }) } Check the demo | Source on GitHub Validation Field level validation and display on field can be added by implement ValidateFunc ,\nand set the web.ValidationErrors result: 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) \u003c 10 { err.FieldError(\"Name\", \"name is too short\") } return }) return } const PresetsEditingCustomizationValidationPath = \"/samples/presets-editing-customization-validation\" Check the demo | Source on GitHub We validate the Name of the customer must be longer than 10 If the error happens, If will show below the field "},{"URL":"basics/brand.html","Title":"Brand","Body":"Brand refers to the top area of the left menu bar, we provide two functions BrandTitle and BrandFunc to customize it. Simple customization If you want only to change the brand string, you can use BrandTitle to set the string, the string will be displayed in the brand area with \u003cH1\u003e tag. b.URIPrefix(PresetsBrandTitlePath). BrandTitle(\"QOR5 Admin\") Check the demo | Source on GitHub Full customization When you opt-in to full brand customization, you can use BrandFunc to be responsible for drawing for the entire brand area, such as you can put your own logo image in it. b.URIPrefix(PresetsBrandFuncPath). BrandFunc(func(ctx *web.EventContext) h.HTMLComponent { return vuetify.VCardText( h.H1(\"Admin\").Style(\"color: red;\"), ).Class(\"pa-0\") }) Check the demo | Source on GitHub Profile Profile is below the brand area, where you can put the current user's information or others. We provide ProfileFunc to customize it. b.URIPrefix(PresetsProfilePath).BrandTitle(\"Admin\"). ProfileFunc(func(ctx *web.EventContext) h.HTMLComponent { // Demo logoutURL := \".\" name := \"QOR5\" account := \"hello@getqor.com\" roles := []string{\"Developer\"} return VMenu().OffsetY(true).Children( h.Template().Attr(\"v-slot:activator\", \"{on, attrs}\").Children( VList( VListItem( VListItemAvatar( VAvatar().Class(\"ml-1\").Color(\"secondary\").Size(40).Children( h.Span(string(name[0])).Class(\"white--text text-h5\"), ), ), VListItemContent( VListItemTitle(h.Text(name)), h.Br(), VListItemSubtitle(h.Text(strings.Join(roles, \", \"))), ), ).Class(\"pa-0 mb-2\"), VListItem( VListItemContent( VListItemTitle(h.Text(account)), ), VListItemIcon( VIcon(\"logout\").Small(true).Attr(\"@click\", web.Plaid().URL(logoutURL).Go()), ), ).Class(\"pa-0 my-n4 ml-1\").Dense(true), ).Class(\"pa-0 ma-n4\"), ), ) }) Check the demo | Source on GitHub "},{"URL":"basics/menu.html","Title":"Menu","Body":"Menu refers to the list on the left side of the page, such as the menu of the Demo below contains Customers and Companies. Check the demo | Source on GitHub Menu order Sorting menus is very simple, use MenuOrder to sort menus as you want by slug name . b.URIPrefix(PresetsMenuOrderPath). MenuOrder( \"books\", \"videos\", \"musics\", ) Check the demo | Source on GitHub Menu group and icon MenuGroup can merge multiple items into one group, as shown in the following code. Use MenuIcon on ModelBuilder can set the item icon, and set menu group icon by Icon following MenuGroup . Icon strings can be found at https://fonts.google.com/icons . mb := b.Model(\u0026book{}).MenuIcon(\"book\") mb.Listing().PageFunc(func(ctx *web.EventContext) (r web.PageResponse, err error) { r.Body = vuetify.VContainer( h.Div( h.H1(\"book\"), ).Class(\"text-center mt-8\"), ) return }) b.MenuOrder( \"books\", b.MenuGroup(\"Media\").SubItems( \"videos\", \"musics\", ).Icon(\"perm_media\"), ) Check the demo | Source on GitHub "},{"URL":"presets-guide/detail-page-for-complex-object.html","Title":"Detailing","Body":"By default, presets will only generate the listing page, editing page for a model,\nIt's for simple objects. But for a complicated object with a lots of relationships and connections,\nand as the main data model of your system, It's better to have detail page for them. In there\nYou can add all kinds of operations conveniently. 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(\u0026Note{}) 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(\u0026notes).Error if err != nil { panic(err) } dt := vx.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 vx.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(\u0026Note{}). 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\" Check the demo | Source on GitHub The name of detailing fields are just a place holder for decide ordering CellComponentFunc customize how the cell display vx.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 vx.Card display a card with toolbar you can setup action buttons We reference the new form drawer that b.Model(\u0026Note{}) creates, but hide notes in the menu Details Info components and actions A vx.DetailInfo component is used for display main detail field of the model.\nAnd you can add any actions to the detail page with ease: 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(\u0026CreditCard{}) 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 := vx.DetailInfo( vx.DetailColumn( vx.DetailField(vx.OptionalText(cu.Name).ZeroLabel(\"No Name\")).Label(\"Name\"), vx.DetailField(vx.OptionalText(cu.Email).ZeroLabel(\"No Email\")).Label(\"Email\"), vx.DetailField(vx.OptionalText(cusID).ZeroLabel(\"No ID\")).Label(\"ID\"), vx.DetailField(vx.OptionalText(cu.CreatedAt.Format(\"Jan 02,15:04 PM\")).ZeroLabel(\"\")).Label(\"Created\"), vx.DetailField(vx.OptionalText(termAgreed).ZeroLabel(\"Not Agreed Yet\")).Label(\"Terms Agreed\"), ).Header(\"ACCOUNT INFORMATION\"), vx.DetailColumn( vx.DetailField(h.RawHTML(cu.Description)).Label(\"Description\"), ).Header(\"DETAILS\"), ) return vx.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(id string, ctx *web.EventContext) (err error) { if ctx.R.FormValue(\"Agree\") != \"true\" { ve := \u0026web.ValidationErrors{} ve.GlobalError(\"You must agree the terms\") err = ve return } err = db.Model(\u0026Customer{}).Where(\"id = ?\", id). Updates(map[string]interface{}{\"term_agreed_at\": time.Now()}).Error return }).ComponentFunc(func(id 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\" Check the demo | Source on GitHub 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 More Usage for Data Table A vx.DataTable component is very featured rich, Here check out the row expandable example: 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(\u0026CreditCard{}) 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(\u0026cards).Error if err != nil { panic(err) } dt := vx.DataTable(cards). WithoutHeader(true). RowExpandFunc(func(obj interface{}, ctx *web.EventContext) h.HTMLComponent { card := obj.(*CreditCard) return vx.DetailInfo( vx.DetailColumn( vx.DetailField(vx.OptionalText(card.Name).ZeroLabel(\"No Name\")).Label(\"Name\"), vx.DetailField(vx.OptionalText(card.Number).ZeroLabel(\"No Number\")).Label(\"Number\"), vx.DetailField(vx.OptionalText(card.ExpireYearMonth).ZeroLabel(\"No Expires\")).Label(\"Expires\"), vx.DetailField(vx.OptionalText(card.Type).ZeroLabel(\"No Type\")).Label(\"Type\"), vx.DetailField(vx.OptionalText(card.Phone).ZeroLabel(\"No phone provided\")).Label(\"Phone\"), vx.DetailField(vx.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 vx.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(\u0026CreditCard{}). 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\" Check the demo | Source on GitHub RowExpandFunc config the content when data table row expand cc.Editing setup the fields when edit cc.Creating setup the fields when create "},{"URL":"basics/layout.html","Title":"Layout","Body":"Presets comes with a built-in layout that works out of the box. And there are some ways to customzie the layout/theme. Theme Presets UI is based on Vuetify , you can modify the Admin theme by configuring the Vuetify options presetsBuilder.VuetifyOptions(` { icons: { iconfont: 'md', }, theme: { themes: { light: { primary: \"#673ab7\", secondary: \"#009688\", accent: \"#ff5722\", error: \"#f44336\", warning: \"#ff9800\", info: \"#8bc34a\", success: \"#4caf50\" }, }, }, } `) Assets If you need third-party front-end libraries to achieve some functions,\nyou can inject them via the ExtraAsset method, and they will be automatically served. presetsBuilder.ExtraAsset(\"/redactor.js\", \"text/javascript\", richeditor.JSComponentsPack()) presetsBuilder.ExtraAsset(\"/redactor.css\", \"text/css\", richeditor.CSSComponentsPack()) you can also call Injector in AssetFunc to add meta, add custom HTML in HEAD and TAIL. presetsBuilder.AssetFunc(func(ctx *web.EventContext) { ctx.Injector.Meta(web.MetaKey(\"charset\"), \"charset\", \"utf8\") ctx.Injector.HeadHTML(`\u003cscript src=\"https://cdn.example.com/hello.js\"\u003e\u003c/script\u003e`) }) Layout You can change the entire layout via LayoutFunc . The default layout is https://github.com/qor5/admin/blob/1e97c0dd45615fb7593245575ab0fea4f98c58b3/presets/presets.go#L860-L969 Layout Options We also provide some options to tweak the layout modelBuilder.LayoutConfig(\u0026presets.LayoutConfig{ SearchBoxInvisible: true, NotificationCenterInvisible: true, }) Plain Layout And We provide PlainLayout which has no UI content except necessary assets.\nIt will be helpful when there are some pages completely independent of Presets layout but still need to be consistent with the Presets theme. "},{"URL":"basics/login.html","Title":"Login","Body":"Login package provides comprehensive login authentication logic and related UI interfaces. It is designed to simplify the process of adding user authentication to QOR5-based backend development project. In QOR5 admin development, we recommend using github.com/qor5/admin/login , which wraps github.com/qor5/x/login to keep the theme of login UI consistent with Presets and provide more powerful features. Basic Usage The example shows how to enable both username/password login and OAuth login. import ( \"net/http\" \"os\" \"github.com/markbates/goth/providers/github\" \"github.com/markbates/goth/providers/google\" plogin \"github.com/qor5/admin/login\" \"github.com/qor5/admin/presets\" . \"github.com/qor5/ui/vuetify\" \"github.com/qor5/web\" \"github.com/qor5/x/login\" . \"github.com/theplant/htmlgo\" \"gorm.io/gorm\" ) type User struct { gorm.Model Name string Address string login.UserPass login.OAuthInfo login.SessionSecure } func serve() { pb := presets.New() lb := plogin.New(pb). DB(DB). UserModel(\u0026User{}). Secret(os.Getenv(\"LOGIN_SECRET\")). OAuthProviders( \u0026login.Provider{ Goth: google.New(os.Getenv(\"LOGIN_GOOGLE_KEY\"), os.Getenv(\"LOGIN_GOOGLE_SECRET\"), os.Getenv(\"BASE_URL\")+\"/auth/callback?provider=google\"), Key: \"google\", Text: \"Google\", }, \u0026login.Provider{ Goth: github.New(os.Getenv(\"LOGIN_GITHUB_KEY\"), os.Getenv(\"LOGIN_GITHUB_SECRET\"), os.Getenv(\"BASE_URL\")+\"/auth/callback?provider=github\"), Key: \"github\", Text: \"Login with Github\", }, ) pb.ProfileFunc(func(ctx *web.EventContext) HTMLComponent { return A(Text(\"logout\")).Href(lb.LogoutURL) }) r := http.NewServeMux() r.Handle(\"/\", pb) lb.Mount(r) mux := http.NewServeMux() mux.Handle(\"/\", lb.Middleware()(r)) http.ListenAndServe(\":8080\", nil) } Username/Password Login To enable Username/Password login, the UserModel needs to implement the UserPasser interface. There is a default implementation - UserPass . type User struct { gorm.Model login.UserPass } Change Password There are three ways to change the password: 1. Visit the default change password page. 2. Call the OpenChangePasswordDialogEvent event to change it in dialog. VBtn(\"Change Password\").OnClick(plogin.OpenChangePasswordDialogEvent) 3. Change the password directly in Editing. userModelBuilder.Editing().Field(\"Password\"). SetterFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) (err error) { u := obj.(*User) if v := ctx.R.FormValue(field.Name); v != \"\" { u.Password = v u.EncryptPassword() } return nil }) MaxRetryCount By default, it allows 5 login attempts with incorrect credentials, and if the limit is exceeded, the user will be locked for 1 hour. This helps to prevent brute-force attacks on the login system. You can call MaxRetryCount to set the maximum retry count. If you set MaxRetryCount to a value less than or equal to 0, it means there is no limit of login attempts, and the user will not be locked after a certain number of failed login attempts. loginBuilder.MaxRetryCount(count) TOTP There is TOTP (Time-based One-time Password) functionality out of the box, which is enabled by default. loginBuilder.TOTP(enable, login.TOTPConfig{ Issuer: \"Issuer\", }) Google reCAPTCHA Google reCAPTCHA is disabled by default. loginBuilder.Recaptcha(enable, login.RecaptchaConfig{ SiteKey: \"SiteKey\", SecretKey: \"SecretKey\", }) OAuth Login OAuth login is based on goth . OAuth login does not require a UserModel . If there is a UserModel , it needs to implement the OAuthUser interface. There is a default implementation - OAuthInfo . type User struct { gorm.Model login.OAuthInfo } Session Secure The SessionSecurer provides a way to manage unique salt for a user record. There is a default implementation - SessionSecure . type User struct { gorm.Model login.UserPass login.OAuthInfo login.SessionSecure } SessionSecurer helps to ensure user security even in the event of secret leakage. When a user logs in, SessionSecurer generates a random salt and associates it with the user's record. This salt is then used to sign the user's session token. When the user makes requests to the server, the server verifies that the session token has been signed with the correct salt. If the salt has been changed, the session token is considered invalid and the user is logged out. Hooks Hooks are functions that are called before or after certain events. The following hooks are available: BeforeSetPassword Extra Values password This hook is called before resetting or changing a password. The hook can be used to validate password formats. AfterLogin This hook is called after a successful login. AfterFailedToLogin Extra Values login error This hook is called after a failed login. Note that the user parameter may be nil. AfterUserLocked This hook is called after a user is locked. AfterLogout This hook is called after a logout. AfterConfirmSendResetPasswordLink Extra Values reset link This hook is called after confirming the sending of a password reset link. This is where the code to send the reset link to the user should be written. AfterResetPassword This hook is called after a password is reset. AfterChangePassword This hook is called after a password is changed. AfterExtendSession Extra Values old session token This hook is called after a session is extended. AfterTOTPCodeReused This hook is called after a TOTP code has been reused. AfterOAuthComplete This hook is called after an OAuth authentication is completed. Customize Pages To customize pages, there are two ways: 1. Each page has a corresponding xxxPageFunc to rewrite the page content. You can easily customize a page by copying the default page func and modifying it according to your needs. loginBuilder.LoginPageFunc(func(ctx *web.EventContext) (r web.PageResponse, err error) { r.Body = Text(\"This is login page\") return }) 2. Only mount the API and serve the login pages manually. When you want to embed the login form into an existing page, this way can be very useful. loginBuilder.LoginPageURL(\"/custom-login-page\") loginBuilder.MountAPI(mux) mux.Handle(\"/custom-login-page\", loginPage) "},{"URL":"presets-guide/permissions.html","Title":"Permissions","Body":"QOR5 permission is based on https://github.com/ory/ladon . A piece of policy looks like this: Who is able to do what on something (with given some context ) perm.PolicyFor(Who).WhoAre(Able).ToDo(What).On(Something).Given(Context) Who - Subject Typically in admin system, they are roles like Admin , Super Admin . Use SubjectsFunc to fetch current subjects: permBuilder.SubjectsFunc(func(r *http.Request) []string { return subjects_like_user_roles }) Able - Effect perm.Allowed perm.Denied What - Action presets has a list of actions: presets.PermList presets.PermGet presets.PermCreate presets.PermUpdate presets.PermDelete And you can define other specific actions if needed. Something - Resource An arbitrary unique resource name. The presets builtin resource format is :presets:mg_menu_group:uri:resource_rn:f_field: . For example :presets:user_management:users:1: represents the user record with id 1 under uri user_management. Use * as wildcard. Context - Condition Optional. The current context that containing condition information about the resource. Use ContextFunc to set the context: permBuilder.ContextFunc(func(r *http.Request, objs []interface{}) perm.Context { c := make(perm.Context) for _, obj := range objs { switch v := obj.(type) { case resource1: c[\"owner\"] = v.Owner // ...other resource cases } } return c }) Policy uses Given to set conditions: perm.PolicyFor(Who).WhoAre(Able).ToDo(What).On(\"*:resource1:*\").Given(perm.Conditions{ \"owner\": \u0026ladon.EqualsSubjectCondition{}, }) Custom Action Let's say there is a button on User detailing page used to ban the user. And only super_admin users have permission to execute this action. First, create a verifier verifier := perm.NewVerifier(\"module_users\", presetsBuilder.GetPermission()) Then inject this verifier to relevant logic, such as whether to show the ban button. validate permission before execute the ban action. if verifier.Do(\"ban\").ObjectOn(user).WithReq(r).IsAllowed() == nil { // ui: show the ban button // action: can execute the ban action } Finally, add policy perm.PolicyFor(\"super_admin\").WhoAre(perm.Allowed).ToDo(\"ban\").On(\":module_users:*\") Example presetsBuilder.Permission( perm.New().Policies( // admin can do anything perm.PolicyFor(\"admin\").WhoAre(perm.Allowed).ToDo(perm.Anything).On(perm.Anything), // viewer can view anything except users perm.PolicyFor(\"viewer\").WhoAre(perm.Allowed).ToDo(presets.PermRead...).On(perm.Anything), perm.PolicyFor(\"viewer\").WhoAre(perm.Denied).ToDo(perm.Anything).On(\"*:users:*\"), // editor can edit their own articles perm.PolicyFor(\"editor\").WhoAre(perm.Allowed).ToDo(perm.Anything).On(\"*:articles:*\").Given(perm.Conditions{ \"owner_id\": \u0026ladon.EqualsSubjectCondition{}, }), ).SubjectsFunc(func(r *http.Request) (ss []string) { user := getCurrentUser(r) ss = append(ss, user.ID) ss = append(ss, user.Roles...) return ss }).ContextFunc(func(r *http.Request, objs []interface{}) perm.Context { c := make(perm.Context) for _, obj := range objs { switch v := obj.(type) { case *Article: c[\"owner_id\"] = v.OwnerID } } return c }), ) Debug perm.Verbose = true prints permission logs which is very helpful for debugging the permission policies: have permission: true, req: \u0026ladon.Request{Resource:\":presets:articles:\", Action:\"presets:list\", Subject:\"viewer\", Context:ladon.Context(nil)} have permission: true, req: \u0026ladon.Request{Resource:\":presets:articles:articles:1:\", Action:\"presets:update\", Subject:\"viewer\", Context:ladon.Context(nil)} have permission: false, req: \u0026ladon.Request{Resource:\":presets:articles:articles:2:\", Action:\"presets:update\", Subject:\"viewer\", Context:ladon.Context(nil)} "},{"URL":"presets-guide/role.html","Title":"Role","Body":"Role provides a UI interface to manage roles(subjects) and their permissions. 1. enable permission DBPolicy perm.New(). Policies( // static policies ). DBPolicy(perm.NewDBPolicy(db)) 2. configure role set resources that you want to manage on interface rb := role.New(db). Resources([]*vuetify.DefaultOptionItem{ {Text: \"All\", Value: \"*\"}, {Text: \"Posts\", Value: \"*:posts:*\"}, {Text: \"Customers\", Value: \"*:customers:*\"}, {Text: \"Products\", Value: \"*:products:*\"}, }) (optional) set actions, the default value is the following // default value rb.Actions([]*vuetify.DefaultOptionItem{ {Text: \"All\", Value: \"*\"}, {Text: \"List\", Value: presets.PermList}, {Text: \"Get\", Value: presets.PermGet}, {Text: \"Create\", Value: presets.PermCreate}, {Text: \"Update\", Value: presets.PermUpdate}, {Text: \"Delete\", Value: presets.PermDelete}, }) (optional) set editor subject to set who can edit Role rb.EditorSubject(\"RoleEditor\") attach role to presets builder rb.Configure(presetsBuilder) "},{"URL":"basics/notification-center.html","Title":"Notification Center","Body":"To enable notification center: Call NotificationFunc on presets.Builder With 2 function parameters\nlike this builder.NotificationFunc(NotifierComponent(), NotifierCount()) The first function is for rendering the content of the popup after user clicked the \"bell icon\".\nThe second function is for rendering the number at the top right corner of the \"bell icon\". import ( \"github.com/qor5/admin/presets\" \"github.com/qor5/admin/presets/gorm2op\" \"github.com/qor5/docs/docsrc/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(\u0026utils.Page{}) b.Model(\u0026utils.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 } } Check the demo | Source on GitHub "},{"URL":"basics/shortcut.html","Title":"Keyboard Shortcut","Body":"To add keyboard shortcut to a button: Trigger the event by GlobalEvents .\nYou 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.\nIn the example, the event would only be triggered when locals.shortCutEnabled is opened. 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) =\u003e 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\" Check the demo | Source on GitHub "},{"URL":"basics/confirm-dialog.html","Title":"Confirm Dialog","Body":"presets.OpenConfirmDialog is a pre-defined event used to show a confirm dialog for user to do confirm before executing the actual action. Queries presets.ConfirmDialogConfirmEvent required Usually the value will be web.Plaid().EventFunc(the actual action event)....Go() . presets.ConfirmDialogPromptText optional To customize the prompt text. presets.ConfirmDialogDialogPortalName optional To use a custom portal for dialog. Example vuetify.VBtn(\"Delete File\"). Attr(\"@click\", web.Plaid(). EventFunc(presets.OpenConfirmDialog). Query(presets.ConfirmDialogConfirmEvent, `alert(\"file deleted\")`, ). Go(), ), Check the demo | Source on GitHub "},{"URL":"slug.html","Title":"Slug","Body":"Slug provides an easy way to create pretty URLs for your model. Usage If the source field called Name , Use *WithSlug which is NameWithSlug as the slug field name, the field type should be slug.Slug . Then the pretty URL would be derived from Name automatically on editing. type User struct {\n\tgorm.Model\n\tName string\n\tNameWithSlug slug.Slug\n} "},{"URL":"seo.html","Title":"SEO","Body":"The SEO library facilitates the optimization of Search Engine results by managing and injecting dynamic data into HTML tags. Usage Initialize a Collection instance. The Collection manages all the registered models and hold global seo settings collection := seo.NewCollection()\n\n// Turn off the default inherit the upper level SEO data when the current SEO data is missing\ncollection.SetInherited(false) Register models to SEO // Register mutiple SEO by name\ncollection.RegisterSEOByNames(\"Product\", \"Announcement\")\n\n// Register a SEO by model\ntype Product struct{\n\tName string\n\tSetting Setting\n}\ncollection.RegisterSEO(\u0026Product{}) Remove models from SEO // Remove by struct\ncollection.RemoveSEO(\u0026Product{})\n// Remove by name\ncollection.RemoveSEO(\"Not Found\") Configuration Change the default global SEO name collection.SetGlobalName(\"My Global SEO\") Change the default context db key collection.SetDBContextKey(\"My DB\") Change the default SEO name collection.RegisterSEO(\u0026Product{}).SetName(\"My Product\") Register customized variables collection.RegisterSEO(\u0026Product{}).\n\tRegisterContextVariables(\"og:image\", func(obj interface{}, _ *Setting, _ *http.Request) string {\n\t\t// this will render \"og:image\" with the value of the object in the current request\n\t\treturn obj.image.url\n\t}).\n\tRegisterContextVariables(\"Name\", func(obj interface{}, _ *Setting, _ *http.Request) string {\n\t\treturn obj.Name\n\t}) Register setting variable This variable will be saved in the database and available as a global variable while editing SEO settings. collection.RegisterSEO(\u0026Product{}).RegisterSettingVaribles(struct{ProductTag string}{}) Render SEO html data // Render Global SEO\ncollection.RenderGlobal(request)\n\n// Render SEO by name\ncollection.Render(\"product\", request)\n\n// Render SEO by model\ncollection.Render(Product{}, request) Customization You can customize your SEO settings by implementing the interface and adding functions such as l10n and publish. type QorSEOSettingInterface interface { GetName() string SetName(string) GetSEOSetting() Setting SetSEOSetting(Setting) GetVariables() Variables SetVariables(Variables) GetLocale() string SetLocale(string) GetTitle() string GetDescription() string GetKeywords() string GetOpenGraphTitle() string GetOpenGraphDescription() string GetOpenGraphURL() string GetOpenGraphType() string GetOpenGraphImageURL() string GetOpenGraphImageFromMediaLibrary() media_library.MediaBox GetOpenGraphMetadata() []OpenGraphMetadata } Suppose MySEOSetting implemented the above interface type MySEOSetting struct{\n\t\tQorSEOSetting\n\t\t// publish\n\t\t// l10n\n} Use SetSettingModel function to set it collection.SetSettingModel(\u0026MySEOSetting{}) Example var SeoCollection *seo.Collection func ConfigureSeo(b *presets.Builder, db *gorm.DB) { SeoCollection = seo.NewCollection() SeoCollection.RegisterSEO(\u0026models.Post{}).RegisterContextVariables( \"Title\", func(object interface{}, _ *seo.Setting, _ *http.Request) string { if article, ok := object.(models.Post); ok { return article.Title } return \"\" }, ).RegisterSettingVaribles(struct{ Test string }{}) SeoCollection.RegisterSEOByNames(\"Product\", \"Announcement\") SeoCollection.Configure(b, db) } Definition Collection manages all the registered models and hold global seo settings. type Collection struct { registeredSEO []*SEO globalName string //default name is GlobalSEO inherited bool //default is true. the order is model seo setting, system seo setting, global seo setting dbContextKey interface{} // get db from context settingModel interface{} // db model afterSave func(ctx context.Context, settingName string, locale string) error // hook called after saving } SEO provides system-level default page matadata. type SEO struct { name string modelTyp reflect.Type contextVariables map[string]contextVariablesFunc // fetch context variables from request settingVariables interface{} // fetch setting variables from db } You can use seo setting at the model level, but you need to register the model to the system SEO type Product struct { Name string SEO Setting } collection.RegisterSEO(\u0026Product{}) "},{"URL":"activity-log.html","Title":"Activity Log","Body":"QOR5 provides a built-in activity module for recording model operations that may be important for admin users of CMS. These records are designed to be easily queried and audited, and the activity module supports the following features: Detailed change logging functionality for model data modifications. Allow certain fields to be ignored when comparing modified data, such as the update time. Customization of the diffing process for complex field types, like time.Time. Customization of the keys used to identify model data. Support both automatic and manual CRUD operation recording. Provide flexibility to customize the actions other than default CRUD. An page for querying the activity log via QOR5 admin Initialize the activity package To initialize activity package with the default configuration, you need to pass a presets.Builder instance and a database instance. presetsBuilder := presets.New() db, err := gorm.Open(postgres.Open(os.Getenv(\"DB_PARAMS\")), \u0026gorm.Config{}) if err != nil { panic(err) } activityBuilder := activity.New(presetsBuilder, db) By default, the activity package uses QOR5 login package's login.UserKey as the default key to fetch the current user from the context. If you want to use your own key, you can use the SetCreatorContextKey function. Same with above, the activity package uses the db instance that passed in during initialization to perform db operations. If you need another db to do the work, you can use SetDBContextKey method. Register the models that require activity tracking This example demonstrates how to register Product into the activity. The activities on the product model will be automatically recorded when it is created, updated, or deleted. type Product struct { Title string Code string Price float64 } productModel := presetsBuilder.Model(\u0026Product{}) activityBuilder.RegisterModel(productModel).EnableActivityInfoTab().AddKeys(\"Title\").AddIgnoredFields(\"Code\").SkipDelete() By default, the activity package will use the primary key as the key to indentify the current model data. You can use SetKeys and AddKeys methods to customize it. When diffing the modified data, the activity package will ignore the ID , CreatedAt , UpdatedAt , DeletedAt fields. You can either use AddIgnoredFields to append your own fields to the default ignored fields. Or SetIgnoredFields method to replace the default ignored fields. For special fields like time.Time or media files handled by QOR5 media_library, activity package already handled them. You can use AddTypeHanders method to handle your own field types. If you want to skip the automatic recording, you can use SkipCreate , SkipUpdate and SkipDelete methods. The Activity package allows for displaying the activities of a record on its editing page. Simply use the EnableActivityInfoTab method to enable this feature. Once enabled, you can customize the format of each activity's display text using the SetTabHeading method. Additionally, you can make each activity a link to the corresponding record using the SetLink method. Record the activity log manually If you register a preset model into the activity, the activity package will automatically record the activity log for CRUD operations. However, if you need to manually record the activity log for other operations or if you want to register a non-preset model, you can use the following sample code. currentCtx := context.WithValue(context.Background(), activity.CreatorContextKey, \"user1\") activityBuilder.AddRecords(\"Publish\", currentCtx, \u0026Product{Title: \"Product 1\", Code: \"P1\", Price: 100}) activityBuilder.AddRecords(\"Update Price\", currentCtx, \u0026Product{Title: \"Product 1\", Code: \"P1\", Price: 200}) "},{"URL":"basics/worker.html","Title":"Worker","Body":"Worker runs a single Job in the background, it can do so immediately or at a scheduled time. Once registered with QOR Admin, Worker will provide a Workers section in the navigation tree, containing pages for listing and managing the following aspects of Workers: All Jobs. Running: Jobs that are currently running. Scheduled: Jobs which have been scheduled to run at a time in the future. Done: finished Jobs. Errors: any errors output from any Workers that have been run. Note The default que GoQueQueue( https://github.com/tnclong/go-que ) only supports postgres for now. To make a job abortable, you need to check ctx.Done() channel in job handler and stop the handler func. Example import ( \"context\" \"errors\" \"fmt\" \"time\" \"github.com/qor5/admin/presets\" \"github.com/qor5/admin/worker\" ) func MountWorker(b *presets.Builder) { wb := worker.New(DB) wb.Configure(b) defer wb.Listen() addJobs(wb) } func addJobs(w *worker.Builder) { w.NewJob(\"noArgJob\"). Handler(func(ctx context.Context, job worker.QorJobInterface) error { job.AddLog(\"hoho1\") job.AddLog(\"hoho2\") job.AddLog(\"hoho3\") return nil }) type ArgJobResource struct { F1 string F2 int F3 bool } argJb := w.NewJob(\"argJob\"). Resource(\u0026ArgJobResource{}). Handler(func(ctx context.Context, job worker.QorJobInterface) error { jobInfo, _ := job.GetJobInfo() job.AddLog(fmt.Sprintf(\"Argument %#+v\", jobInfo.Argument)) return nil }) // you can to customize the resource Editing via GetResourceBuilder() argJb.GetResourceBuilder().Editing() w.NewJob(\"progressTextJob\"). Handler(func(ctx context.Context, job worker.QorJobInterface) error { job.AddLog(\"hoho1\") job.AddLog(\"hoho2\") job.AddLog(\"hoho3\") job.SetProgressText(`\u003ca href=\"https://www.google.com\"\u003eDownload users\u003c/a\u003e`) return nil }) // check ctx.Done() to stop the handler w.NewJob(\"longRunningJob\"). Handler(func(ctx context.Context, job worker.QorJobInterface) error { for i := 1; i \u003c= 5; i++ { select { case \u003c-ctx.Done(): job.AddLog(\"job aborted\") return nil default: job.AddLog(fmt.Sprintf(\"%v\", i)) job.SetProgress(uint(i * 20)) time.Sleep(time.Second) } } return nil }) // insert worker.Schedule to resource to make a job schedulable type ScheduleJobResource struct { F1 string worker.Schedule } w.NewJob(\"scheduleJob\"). Resource(\u0026ScheduleJobResource{}). Handler(func(ctx context.Context, job worker.QorJobInterface) error { jobInfo, _ := job.GetJobInfo() job.AddLog(fmt.Sprintf(\"%#+v\", jobInfo.Argument)) return nil }) w.NewJob(\"errorJob\"). Handler(func(ctx context.Context, job worker.QorJobInterface) error { job.AddLog(\"=====perform error job\") return errors.New(\"imError\") }) w.NewJob(\"panicJob\"). Handler(func(ctx context.Context, job worker.QorJobInterface) error { job.AddLog(\"=====perform panic job\") panic(\"letsPanic\") }) } Check the demo | Source on GitHub Action Worker Action Worker is used to visualize the progress of long-running actions. import ( \"context\" \"fmt\" \"time\" \"github.com/qor5/admin/presets\" \"github.com/qor5/admin/worker\" \"github.com/qor5/ui/vuetify\" \"github.com/qor5/web\" h \"github.com/theplant/htmlgo\" \"gorm.io/gorm\" ) type ExampleResource struct { gorm.Model Name string } func MountActionWorker(b *presets.Builder) { mb := b.Model(\u0026ExampleResource{}) mb.Listing().ActionsAsMenu(true) wb := worker.New(DB) wb.Configure(b) defer wb.Listen() addActionJobs(mb, wb) } func addActionJobs(mb *presets.ModelBuilder, wb *worker.Builder) { lb := mb.Listing() noParametersJob := wb.ActionJob( \"No parameters\", mb, func(ctx context.Context, job worker.QorJobInterface) error { for i := 1; i \u003c= 10; i++ { select { case \u003c-ctx.Done(): job.AddLog(\"job aborted\") return nil default: job.SetProgress(uint(i * 10)) time.Sleep(time.Second) } } job.SetProgressText(`\u003ca href=\"https://qor5-test.s3.ap-northeast-1.amazonaws.com/system/media_libraries/37/file.@qor_preview.png\"\u003ePlease download this file\u003c/a\u003e`) return nil }, ).Description(\"This test demo is used to show that an no parameter job can be executed\") parametersBoxJob := wb.ActionJob( \"Parameter input box\", mb, func(ctx context.Context, job worker.QorJobInterface) error { for i := 1; i \u003c= 10; i++ { select { case \u003c-ctx.Done(): job.AddLog(\"job aborted\") return nil default: job.SetProgress(uint(i * 10)) time.Sleep(time.Second) } } job.SetProgressText(`\u003ca href=\"https://qor5-test.s3.ap-northeast-1.amazonaws.com/system/media_libraries/37/file.@qor_preview.png\"\u003ePlease download this file\u003c/a\u003e`) return nil }, ).Description(\"This test demo is used to show that an input box when there are parameters\"). Params(\u0026struct{ Name string }{}) displayLogJob := wb.ActionJob( \"Display log\", mb, func(ctx context.Context, job worker.QorJobInterface) error { for i := 1; i \u003c= 10; i++ { select { case \u003c-ctx.Done(): job.AddLog(\"job aborted\") return nil default: job.SetProgress(uint(i * 10)) job.AddLog(fmt.Sprintf(\"%v\", i)) time.Sleep(time.Second) } } job.SetProgressText(`\u003ca href=\"https://qor5-test.s3.ap-northeast-1.amazonaws.com/system/media_libraries/37/file.@qor_preview.png\"\u003ePlease download this file\u003c/a\u003e`) return nil }, ).Description(\"This test demo is used to show the log section of this job\"). Params(\u0026struct{ Name string }{}). DisplayLog(true). ProgressingInterval(4000) getArgsJob := wb.ActionJob( \"Get Args\", mb, func(ctx context.Context, job worker.QorJobInterface) error { jobInfo, err := job.GetJobInfo() if err != nil { return err } job.AddLog(fmt.Sprintf(\"Action Params Name is %#+v\", jobInfo.Argument.(*struct{ Name string }).Name)) job.AddLog(fmt.Sprintf(\"Origina Context AuthInfo is %#+v\", jobInfo.Context[\"AuthInfo\"])) job.AddLog(fmt.Sprintf(\"Origina Context URL is %#+v\", jobInfo.Context[\"URL\"])) for i := 1; i \u003c= 10; i++ { select { case \u003c-ctx.Done(): return nil default: job.SetProgress(uint(i * 10)) time.Sleep(time.Second) } } job.SetProgressText(`\u003ca href=\"https://qor5-test.s3.ap-northeast-1.amazonaws.com/system/media_libraries/37/file.@qor_preview.png\"\u003ePlease download this file\u003c/a\u003e`) return nil }, ).Description(\"This test demo is used to show how to get the action's arguments and original page context\"). Params(\u0026struct{ Name string }{}). DisplayLog(true). ContextHandler(func(ctx *web.EventContext) map[string]interface{} { auth, err := ctx.R.Cookie(\"auth\") if err == nil { return map[string]interface{}{\"AuthInfo\": auth.Value} } return nil }) lb.Action(\"Action Job - No parameters\"). ButtonCompFunc( func(ctx *web.EventContext) h.HTMLComponent { return vuetify.VBtn(\"Action Job - No parameters\").Color(\"secondary\").Depressed(true).Class(\"ml-2\"). Attr(\"@click\", noParametersJob.URL()) }) lb.Action(\"Action Job - Parameter input box\"). ButtonCompFunc( func(ctx *web.EventContext) h.HTMLComponent { return vuetify.VBtn(\"Action Job - Parameter input box\").Color(\"secondary\").Depressed(true).Class(\"ml-2\"). Attr(\"@click\", parametersBoxJob.URL()) }) lb.Action(\"Action Job - Display log\"). ButtonCompFunc( func(ctx *web.EventContext) h.HTMLComponent { return vuetify.VBtn(\"Action Job - Display log\").Color(\"secondary\").Depressed(true).Class(\"ml-2\"). Attr(\"@click\", displayLogJob.URL()) }) lb.Action(\"Action Job - Get Args\"). ButtonCompFunc( func(ctx *web.EventContext) h.HTMLComponent { return vuetify.VBtn(\"Action Job - Get Args\").Color(\"secondary\").Depressed(true).Class(\"ml-2\"). Attr(\"@click\", getArgsJob.URL()) }) } Check the demo | Source on GitHub "},{"URL":"basics/publish.html","Title":"Publish","Body":"Publish controls the online/offline status of records. It generalizes publishing using 3 main modules: Status : to flag a record be online/offline Schedule : to schedule records to be online/offline automatically Version : to allow a record to have more than one copies and chain them together Usage Inject modules to the resource model. type Product struct { gorm.Model Name string Price int publish.Status publish.Schedule publish.Version } Implement primary slug interfaces for passing the values of primary keys between events var _ presets.SlugEncoder = (*Product)(nil) var _ presets.SlugDecoder = (*Product)(nil) func (p *Product) PrimarySlug() string { return fmt.Sprintf(\"%v_%v\", p.ID, p.Version.Version) } func (p *Product) PrimaryColumnValuesBySlug(slug string) map[string]string { segs := strings.Split(slug, \"_\") if len(segs) != 2 { panic(\"wrong slug\") } return map[string]string{ \"id\": segs[0], \"version\": segs[1], } } Create publisher and configure Publish view for model, and remember to display Status and Schedule fields in Editing mb := b.Model(\u0026Product{}) mb.Editing(\"StatusBar\", \"Schedule\", \"Name\", \"Price\") publisher := publish.New(DB, nil) publish_view.Configure(b, DB, nil, publisher, mb) // run the publisher job if Schedule is used go publish.RunPublisher(DB, nil, publisher) Implement the publish interfaces if there is a need to publish content to storage(filesystem, AWS S3, ...) var _ publish.PublishInterface = (*Product)(nil) var _ publish.UnPublishInterface = (*Product)(nil) func (p *Product) GetPublishActions(db *gorm.DB, ctx context.Context, storage oss.StorageInterface) (objs []*publish.PublishAction, err error) { // create publish actions return } func (p *Product) GetUnPublishActions(db *gorm.DB, ctx context.Context, storage oss.StorageInterface) (objs []*publish.PublishAction, err error) { // create unpublish actions return } Check the demo | Source on GitHub Modules Status Status module stores the status of the record. const ( StatusDraft = \"draft\" StatusOnline = \"online\" StatusOffline = \"offline\" ) type Status struct { Status string `gorm:\"default:'draft'\"` OnlineUrl string } The initial status is draft , after publishing it becomes online , and after unpublishing it becomes offline . Schedule Schedule module schedules records to be online/offline automatically with the publisher job. type Schedule struct { ScheduledStartAt *time.Time `gorm:\"index\"` ScheduledEndAt *time.Time `gorm:\"index\"` ActualStartAt *time.Time ActualEndAt *time.Time } If a record has ScheduledStartAt set, and the current time is larger than this value, the record will be published and the ActualStartAt will be set to the actual published time, the ScheduledStartAt will be cleared. If a record has ScheduledEndAt set, and the current time is larger than this value, the record will be unpublished and the ActualEndAt will be set to the actual unpublished time, the ScheduledEndAt will be cleared. Version Version module allows one record to have multiple copies, with Schedule, you can even schedule different prices of a product for a whole year. type Version struct { Version string `gorm:\"primary_key;size:128\"` VersionName string ParentVersion string } The Version will be the primary key. By default, the Version value will be YYYY-MM-DD-vSeq , e.g. 2006-01-02-v01 . And you can rename a version on interface, which will modify the value of VersionName . List List module publishes list page of resource. type List struct { PageNumber int Position int ListDeleted bool ListUpdated bool } "},{"URL":"basics/i18n.html","Title":"Internationalization","Body":"The i18n package provides support for internationalization (i18n) in Go applications.\nWith the package, you can support multiple languages,\nregister messages for each module in each language, and serve multilingual content\nbased on the user's preferences. Check the demo | Source on GitHub Getting Started To use the i18n package, you first need to import it into your Go application: import \"github.com/qor5/x/i18n\" Next, create a new Builder instance using the New() function.\nIf you want to use it with QOR5, use the I18n() on presets.Builder : i18nB := b.I18n() The Builder struct is the central point of the i18n package.\nIt holds the supported languages, the messages for each module in each language,\nand the configuration for retrieving the language preference. Adding Support Languages To support multiple languages in your web application, you need to define the languages that you support.\nYou can do this by calling the SupportLanguages function on the Builder struct: i18nB.SupportLanguages(language.English, language.SimplifiedChinese, language.Japanese) The i18n package uses English as the default language. You can add other languages by the SupportLanguages function. Registering Module Messages Once you have defined the languages, you need to register messages for each module.\nYou can do this by the RegisterForModule function on the Builder struct: i18nB. RegisterForModule(language.SimplifiedChinese, presets.ModelsI18nModuleKey, Messages_zh_CN_ModelsI18nModuleKey). RegisterForModule(language.Japanese, presets.ModelsI18nModuleKey, Messages_ja_JP_ModelsI18nModuleKey). RegisterForModule(language.English, I18nExampleKey, Messages_en_US). RegisterForModule(language.Japanese, I18nExampleKey, Messages_ja_JP). RegisterForModule(language.SimplifiedChinese, I18nExampleKey, Messages_zh_CN). GetSupportLanguagesFromRequestFunc(func(r *http.Request) []language.Tag { return b.I18n().GetSupportLanguages() }) The RegisterForModule function takes three arguments: the language tag, the module key,\nand a pointer to a struct that implements the Messages interface.\nThe Messages interface is an empty interface that you can use to define your own messages. Such a struct might look like this: const I18nExampleKey i18n.ModuleKey = \"I18nExampleKey\" type Messages struct { Admin string Welcome string } var Messages_en_US = \u0026Messages{ Admin: \"Admin\", Welcome: \"Welcome\", } var Messages_zh_CN = \u0026Messages{ Admin: \"管理系统\", Welcome: \"欢迎\", } var Messages_ja_JP = \u0026Messages{ Admin: \"管理システム\", Welcome: \"ようこそ\", } If you want to define messages inside the system,\nyou can add new variables to the message structure associated with presets.ModelsI18nModuleKey ,\nand the variable name definitions follow the camel case. Such a struct might look like this: type Messages_ModelsI18nModuleKey struct { Homes string Videos string VideosName string VideosDescription string } var Messages_zh_CN_ModelsI18nModuleKey = \u0026Messages_ModelsI18nModuleKey{ Homes: \"主页\", Videos: \"视频\", VideosName: \"视频名称\", VideosDescription: \"视频描述\", } var Messages_ja_JP_ModelsI18nModuleKey = \u0026Messages_ModelsI18nModuleKey{ Homes: \"ホーム\", Videos: \"ビデオ\", VideosName: \"ビデオの名前\", VideosDescription: \"ビデオの説明\", } The GetSupportLanguagesFromRequestFunc is a method of the Builder struct in the i18n package.\nIt allows you to set a function that retrieves the list of supported languages\nfrom an HTTP request, which can be useful in scenarios where the list of supported\nlanguages varies based on the request context. If you create a separate page, you need to use the EnsureLanguage to get i18n to work on this page. The EnsureLanguage function is an HTTP middleware that ensures the request's language\nis properly set and stored. It does this by first checking the query parameters for\na language value, and if found, setting a cookie with that value. If no language\nvalue is present in the query parameters, it looks for the language value in the cookie. The middleware then determines the best-matching language from the supported languages\nbased on the \"Accept-Language\" header of the request. If no match is found,\nit defaults to the first supported language. It then sets the language context for\nthe request, which can be retrieved later by calling the MustGetModuleMessages function. Retrieving Messages To retrieve module messages in your HTTP handler, you can use the MustGetModuleMessages function: msgr := i18n.MustGetModuleMessages(ctx.R, I18nExampleKey, Messages_en_US).(*Messages) r.Body = v.VContainer( h.Div( h.H1(msgr.Welcome), ).Class(\"text-center mt-8\"), ) The MustGetModuleMessages function takes three arguments:\nthe HTTP request, the module key, and a pointer to a struct\nthat implements the Messages interface. The function retrieves the messages\nfor the specified module in the language set by the i18n middleware. "},{"URL":"basics/l10n.html","Title":"Localization","Body":"L10n gives your models the ability to localize for different Locales. It can be a catalyst for the adaptation of a product, application, or document content to meet the language, cultural, and other requirements of a specific target market. Define a struct Define a struct that requires embed l10n.Locale . Also this struct must implement PrimarySlug() string and PrimaryColumnValuesBySlug(slug string) map[string]string . type L10nModel struct { gorm.Model Title string l10n.Locale } func (lm *L10nModel) PrimarySlug() string { return fmt.Sprintf(\"%v_%v\", lm.ID, lm.LocaleCode) } func (lm *L10nModel) PrimaryColumnValuesBySlug(slug string) map[string]string { segs := strings.Split(slug, \"_\") if len(segs) != 2 { panic(\"wrong slug\") } return map[string]string{ \"id\": segs[0], \"locale_code\": segs[1], } } Init a l10n builder Register locales here. You can use GetSupportLocaleCodesFromRequestFunc to determine who can use which locales. l10nBuilder := l10n.New() l10nBuilder. RegisterLocales(\"International\", \"international\", \"International\"). RegisterLocales(\"China\", \"cn\", \"China\"). RegisterLocales(\"Japan\", \"jp\", \"Japan\"). GetSupportLocaleCodesFromRequestFunc(func(R *http.Request) []string { return l10nBuilder.GetSupportLocaleCodes()[:] }) Configure the model builder Use l10n_view.Configure() func to configure l10n view. The Switch Locale ui will appear below the Brand . The Localize ui will appear in the RowMenuItem under the Edit and the Delete . Localize button is used to copy a piece of data from the current locale to the other locales. mb := b.Model(\u0026L10nModel{}).URIName(\"l10n-models\") l10n_view.Configure(b, DB, l10nBuilder, nil, mb) mb.Listing(\"ID\", \"Title\", \"Locale\") Full Example import ( \"fmt\" \"net/http\" \"strings\" \"github.com/qor5/admin/l10n\" l10n_view \"github.com/qor5/admin/l10n/views\" \"github.com/qor5/admin/presets\" \"github.com/qor5/admin/presets/gorm2op\" \"gorm.io/gorm\" ) type L10nModel struct { gorm.Model Title string l10n.Locale } func (lm *L10nModel) PrimarySlug() string { return fmt.Sprintf(\"%v_%v\", lm.ID, lm.LocaleCode) } func (lm *L10nModel) PrimaryColumnValuesBySlug(slug string) map[string]string { segs := strings.Split(slug, \"_\") if len(segs) != 2 { panic(\"wrong slug\") } return map[string]string{ \"id\": segs[0], \"locale_code\": segs[1], } } func LocalizationExampleMock(b *presets.Builder) { if err := DB.AutoMigrate(\u0026L10nModel{}); err != nil { panic(err) } b.URIPrefix(LocalizationExamplePath). DataOperator(gorm2op.DataOperator(DB)) l10nBuilder := l10n.New() l10nBuilder. RegisterLocales(\"International\", \"international\", \"International\"). RegisterLocales(\"China\", \"cn\", \"China\"). RegisterLocales(\"Japan\", \"jp\", \"Japan\"). GetSupportLocaleCodesFromRequestFunc(func(R *http.Request) []string { return l10nBuilder.GetSupportLocaleCodes()[:] }) mb := b.Model(\u0026L10nModel{}).URIName(\"l10n-models\") l10n_view.Configure(b, DB, l10nBuilder, nil, mb) mb.Listing(\"ID\", \"Title\", \"Locale\") Check the demo | Source on GitHub "},{"URL":"basics/page-func-and-event-func.html","Title":"Page Func and Event Func","Body":"PageFunc is used to build a web page, EventFunc is called when user interact with the page, For example button or link clicks. type PageFunc func(ctx *EventContext) (r PageResponse, err error) type EventFunc func(ctx *EventContext) (r EventResponse, err error) web.Page(...) converts multiple EventFunc s along with one PageFunc to a http.Handler ,\nevent 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 Here is a hello world with more interactions. User click the button will reload the page with latest time 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\" Check the demo | Source on GitHub 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 mux.Handle( e00_basics.HelloWorldReloadPath, e00_basics.HelloWorldReloadPB.Wrap(demoLayout), ) wb.Page(...) convert any PageFunc into http.Handler , outside you can wrap any middleware that can use on Go standard http.Handler . In case you don't know what is a http.Handler middleware,\nIt's a function that takes http.Handler as input, might also with other parameters,\nAnd also return a new http.Handler, gziphandler is an example. But What the heck is demoLayout there?\nWell it's a PageFunc middleware. That takes an PageFunc as input,\nwrap it's PageResponse with layout html and return a new PageFunc .\nIf you follow the code to write your own PageFunc ,\nThe button click might not work without this.\nSince there is no layout to import needed javascript to make this work.\ncontinue to next page to checkout how to add necessary javascript, css etc to make the demo work. "},{"URL":"advanced-functions/the-go-html-builder.html","Title":"The Go HTML builder","Body":"Like at the beginning we said, That we don't use interpreted template language (eg go html/template)\nto 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 is just for that. Although Go can't do flexible builder syntax like Kotlin does,\nBut it can also do quite well. Consider the following code: 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\" It's basically assembled what Kotlin can do, Also is legitimate Go code. Check the demo | Source on GitHub "},{"URL":"presets-guide/its-the-whole-house.html","Title":"Not just scaffolding, it's the whole house","Body":"Presets let you config generalized data management UI interface for database.\nIt's not a scaffolding to generate source code. But provide more abstract and\nflexible API to enrich features along the way. package e21_presents import ( \"fmt\" \"net/url\" \"os\" \"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/postgres\" \"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(postgres.Open(os.Getenv(\"DB_PARAMS\")), \u0026gorm.Config{}) if err != nil { panic(err) } db.Logger.LogMode(logger.Info) err = db.AutoMigrate( \u0026Customer{}, \u0026Company{}, \u0026Address{}, ) 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(\u0026Customer{}) return } const PresetsHelloWorldPath = \"/samples/presets-hello-world\" And this *presets.Builder instance is actually also a http.Handler , So that we can mount it\nto the http serve mux directly like this: c00 := presets.New().AssetFunc(addGA) e21_presents.PresetsHelloWorld(c00) mux.Handle( e21_presents.PresetsHelloWorldPath+\"/\", c00, ) Check the demo | Source on GitHub With r.Model(\u0026Customer{}) : 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 "},{"URL":"vuetify-components/lazy-portals.html","Title":"Lazy Portals","Body":"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. 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 = \u0026mystate{} ctx.MustUnmarshalForm(s) textField := VTextField().FieldName(\"Company\") if len(s.Error) \u003e 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 = \u0026mystate{} ctx.MustUnmarshalForm(s) if len(s.Company) \u003c 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, \u0026web.PortalUpdate{ Name: \"portalC\", Body: h.Text(fmt.Sprint(time.Now().UnixNano())), }, \u0026web.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\" Check the demo | Source on GitHub "},{"URL":"basics/layout-function-and-page-injector.html","Title":"Layout Function and Page Injector","Body":"Read this code first, Guess what it does. func demoLayout(in web.PageFunc) (out web.PageFunc) { return func(ctx *web.EventContext) (pr web.PageResponse, err error) { addGA(ctx) ctx.Injector.HeadHTML(` \u003cscript src='/assets/vue.js'\u003e\u003c/script\u003e `) ctx.Injector.TailHTML(coreJSTags) ctx.Injector.HeadHTML(` \u003cstyle\u003e [v-cloak] { display: none; } \u003c/style\u003e `) var innerPr web.PageResponse innerPr, err = in(ctx) if err != nil { panic(err) } pr.Body = innerPr.Body return } } ctx.Injector is for inject html into default layout's html head, and bottom of body.\nhtml head normally for page title, keywords etc all kinds meta data, and css styles,\njavascript 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: mux.Handle(\"/assets/main.js\", web.PacksHandler(\"text/javascript\", web.JSComponentsPack(), ), ) mux.Handle(\"/assets/vue.js\", web.PacksHandler(\"text/javascript\", web.JSVueComponentsPack(), ), ) web.JSComponentsPack is the production version of QOR5 core javascript code.\nCreated by using @vue/cli ,\nIt does the basic functions like render server side returned html as vue templates.\nProvide basic event functions that call to server, and manage push state\n(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 . import ( \"embed\" ) //go:embed corejs/dist/*.js var box embed.FS func JSComponentsPack() ComponentsPack { v, err := box.ReadFile(\"corejs/dist/core.umd.min.js\") if err != nil { panic(err) } return ComponentsPack(v) } func JSVueComponentsPack() ComponentsPack { v, err := box.ReadFile(\"corejs/dist/vue.min.js\") if err != nil { panic(err) } return ComponentsPack(v) } And with web.PacksHandler , You can merge multiple javascript or css assets together into one url.\nSo that browser only need to request them one time. and cache them. The cache is set to the start\ntime of the process. So next time the app restarts, it invalid the cache. Summary For a new project: Use @vue/cli to create an asset project that manage your javascript and css. and compile them for production use Use 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 "},{"URL":"basics/switch-pages-with-push-state.html","Title":"Switch Pages with Push State","Body":"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 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: 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) \u003e 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) Check the demo | Source on GitHub When running the above demo, If you check Chrome Developer Tools about Network requests,\nYou 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 ,\nBut it is default added to every web.PageFunc . So that the web page can\nboth respond to normal HTTP request from Browser, Search Engine, Or from\nother pages in the same web app that can do push state link. Summary 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: Go to a new page Reload the whole current page 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. "},{"URL":"basics/reload-page-with-a-flash.html","Title":"Reload Page with a Flash","Body":"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: 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 = \u0026Data1{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\" Check the demo | Source on GitHub 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 .\nDifferent 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. "},{"URL":"basics/partial-refresh-with-portal.html","Title":"Partial Refresh with Portal","Body":"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: 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, \u0026web.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\" Check the demo | Source on GitHub web.Portal().Name(\"part1\") Place a placeholder inside you page, and append web.PortalUpdate to er.UpdatePortals to update the portal with that name.\nMultiple portal can be updated at the same time. Load Portal in separate AJAX request With web.Portal , We can also load the portal with a separate AJAX request after page load.\nIt is useful for the type of the content is not that important to the page, But load them are\nquite heavy. Like related products of a product detail page of a ECommerce site. 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(` \u003cstyle\u003e .rp { float: left; width: 200px; height: 200px; margin-right: 20px; background-color: orange; } \u003c/style\u003e `, ) 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 \u003e 5 { er.VarsScript = `vars.interval = 0;` } return } func loadData(ctx *web.EventContext) (er web.EventResponse, err error) { var r []string for i := 0; i \u003c 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\" Check the demo | Source on GitHub 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 \u0026 Async Components , to load Go generated html (vue runtime templates)\nfrom 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. "},{"URL":"basics/manipulate-page-url-in-event-func.html","Title":"Manipulate Page URL in Event Func","Body":"Encode page state into query strings in url is useful. because user can paste the link to another person,\nThat can open the page to the exact state of the page being sent, Not the initial state of the page. For example: 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\")) \u003e 0 { title = ctx.R.URL.Query().Get(\"title\") } var panel HTMLComponent if len(ctx.R.URL.Query().Get(\"panel\")) \u003e 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\" Check the demo | Source on GitHub 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: A().Text(\"change page title\").Href(\"javascript:;\").\n\tAttr(\"@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 "},{"URL":"basics/summary-of-event-response.html","Title":"Summary of Event Response","Body":"The behaviour of web.EventFunc is controlled by it's return type web.EventResponse type EventResponse struct { PageTitle string `json:\"pageTitle,omitempty\"` Body h.HTMLComponent `json:\"body,omitempty\"` Reload bool `json:\"reload,omitempty\"` PushState *LocationBuilder `json:\"pushState\"` // This we don't omitempty, So that {} can be kept when use url.Values{} RedirectURL string `json:\"redirectURL,omitempty\"` // change window url without push state ReloadPortals []string `json:\"reloadPortals,omitempty\"` UpdatePortals []*PortalUpdate `json:\"updatePortals,omitempty\"` Data interface{} `json:\"data,omitempty\"` // used for return collection data like TagsInput data source VarsScript string `json:\"varsScript,omitempty\"` // used with InitContextVars to set values for example vars.show to used by v-model } PageTitle set the html head title, It not only set when render html page directly which is\nrequest the url directly from the browser. Also use javascript to set the page title when you do\npush 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\nhtml 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\nevent 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\nhead 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\nwill marshals to JSON, and on the client side use javascript to utilize them "},{"URL":"basics/scope-component.html","Title":"Scope Component","Body":"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') .\nWe 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: 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) Check the demo | Source on GitHub 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 ) 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 .\nIn this way, you can submit Raw Material ID directly in the Material Form . For example: 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) Check the demo | Source on GitHub Use web.Scope().VSlot(\"{ plaidForm }\") to determine the scope of a form. "},{"URL":"basics/event-handling.html","Title":"Event Handling","Body":"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 .\nThe 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 URL Request a page. 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 } Check the demo | Source on GitHub PushState Reqest a page and also changing the window location. 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 } Check the demo | Source on GitHub Reload Refresh page. 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 } Check the demo | Source on GitHub Query Request a page with a query. 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 } Check the demo | Source on GitHub MergeQuery Request a page with merging a query. 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\u0026name=qor5\u0026email=qor5@theplant.jp\").PushState(true).Query(\"address\", \"tokyo\").MergeQuery(true).Go())), ), ) return } Check the demo | Source on GitHub ClearMergeQuery Request a page with clearing a query. 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\u0026name=qor5\u0026email=qor5@theplant.jp\").PushState(true).Query(\"address\", \"tokyo\").ClearMergeQuery([]string{\"name\"}).Go())), ), ) return } Check the demo | Source on GitHub StringQuery Request a page with a query string. 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 } Check the demo | Source on GitHub Queries Request a page with url.Values. 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 } Check the demo | Source on GitHub PushStateURL Request a page with a url and also changing the window location. 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 } Check the demo | Source on GitHub Location Open a page with more options. 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(\u0026web.LocationBuilder{MyURL: EventExamplePagePath, MyStringQuery: \"address=test\"}).Go())), ), ) return } Check the demo | Source on GitHub FieldValue Fill in a value on form. 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 } Check the demo | Source on GitHub FormClear Clear all form data. 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 } Check the demo | Source on GitHub EventFunc Register an event func and call it when the event is triggered. func EventHandlingEventFunc(ctx *web.EventContext) (pr web.PageResponse, err error) { pr.Body = Div( VBtn(\"Go\").Attr(\"@click\", web.POST().EventFunc(\"hello\").Go()), ) return } Check the demo | Source on GitHub Script Run a script code. 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 } Check the demo | Source on GitHub Raw Directly call the js method 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 } Check the demo | Source on GitHub "},{"URL":"basics/form-handling.html","Title":"Form Handling","Body":"Form handling is an important part of web development. to make handling form easy,\nwe have a global form that always be submitted with any event func. What you need to do\nis just to give an input a name. For example: import ( \"fmt\" \"io\" \"mime/multipart\" \"github.com/qor5/docs/docsrc/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(\u0026fv) 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\" Check the demo | Source on GitHub Use .Attr(web.VFieldName(\"Abc\")...) to set the field name, make the name matches your data struct field name.\nSo that you can ctx.UnmarshalForm(\u0026fv) 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. "},{"URL":"vuetify-components/basic-inputs.html","Title":"Basic Inputs","Body":"Vuetify provides many form basic inputs, and also with error messages display on fields. Here is one example: import ( \"mime/multipart\" \"github.com/qor5/docs/docsrc/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 = \u0026myFormValue{ 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, \u0026web.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 = \u0026myFormValue{} ctx.MustUnmarshalForm(s) verr := web.ValidationErrors{} if len(s.MyValue) \u003c 10 { verr.FieldError(\"MyValue\", \"my value is too small\") } if len(s.TextareaValue) \u003e 5 { verr.FieldError(\"TextareaValue\", \"textarea value is too large\") } if !s.Agreed { verr.FieldError(\"Agreed\", \"You must agree the terms\") } if s.Slider1 \u003e 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) Check the demo | Source on GitHub "},{"URL":"vuetify-components/a-taste-of-using-vuetify-in-go.html","Title":"A Taste of using Vuetify in Go","Body":"Vuetify is a really mature Vue components library for Material Design . We have made the efforts to\nintegrate most all of it as a go package. You can use it with ease just like any\nother go package. Use container, toolbar, list, list item etc This example is purely render, we didn't integrate any interaction (event func) to it. 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.vuetifyjs.com/images/lists/1.jpg\"), ), VListItemContent( VListItemTitle(h.Text(\"Brunch this weekend?\")), VListItemSubtitle( h.Span(\"Ali Connors\").Class(\"text--primary\"), h.Text(\"\u0026mdash; 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.vuetifyjs.com/images/lists/2.jpg\"), ), VListItemContent( VListItemTitle(h.RawHTML(`Summer BBQ \u003cspan class=\"grey--text text--lighten-1\"\u003e4\u003c/span\u003e`)), VListItemSubtitle(h.RawHTML(`\u003cspan class='text--primary'\u003eto Alex, Scott, Jennifer\u003c/span\u003e \u0026mdash; Wish I could come, but I'm out of town this weekend.`)), ), ), VDivider().Inset(true), VListItem( VListItemAvatar( h.Img(\"https://cdn.vuetifyjs.com/images/lists/3.jpg\"), ), VListItemContent( VListItemTitle(h.Text(`Oui oui`)), VListItemSubtitle(h.RawHTML(`\u003cspan class='text--primary'\u003eSandra Adams\u003c/span\u003e \u0026mdash; Do you have Paris recommendations? Have you ever been?`)), ), ), ).TwoLine(true), ) return } var HelloVuetifyListPB = web.Page(HelloVuetifyList) const HelloVuetifyListPath = \"/samples/hello-vuetify-list\" Check the demo | Source on GitHub Use menu, card, list, etc This example uses the menu popup, card, list component. and some interactions of clicking\nbuttons on the menu popup. import ( \"github.com/qor5/docs/docsrc/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(\u0026fv) 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.vuetifyjs.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, \u0026web.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\" .Attr(web.InitContextVars, \"{myMenuShow: false}\") is a special vue directive that\nwe 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\nthe initialization. It's often useful to control dialog, popups. At this example,\nWe add it, So that the cancel button on the menu, could actually close the menu without\nrequesting server backend. toggleFavored event func did an partial update only to the favorite icon button. So that it won't close the\nmenu popup, but updated the button to toggle the favorite icon. Check the demo | Source on GitHub "},{"URL":"vuetify-components/linkage-select.html","Title":"Linkage Select","Body":"LinkageSelect is a component for multi-level linkage select. 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\" Check the demo | Source on GitHub Filter intergation 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(\u0026Address{}) 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\u0026City\u0026District\", 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: \"吴江区\"}, }, } } Check the demo | Source on GitHub "},{"URL":"vuetify-components/auto-complete.html","Title":"Auto Complete","Body":"AutoComplete is a more advanced component that vuetify provides, We extend it\nSo 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 import ( \"fmt\" \"os\" \"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/postgres\" \"gorm.io/gorm\" ) type ( User struct { Login string Name string } UserIcons struct { Login string `json:\"text\"` Name string `json:\"value\"` Icon string `json:\"icon\"` } Product struct { ID uint `gorm:\"primarykey\"` Name string } ) var ( options = []*User{ {Login: \"sam\", Name: \"Sam\"}, {Login: \"john\", Name: \"John\"}, {Login: \"charles\", Name: \"Charles\"}, } iconOptions = []*UserIcons{ {Login: \"sam\", Name: \"Sam\", Icon: \"https://cdn.vuetifyjs.com/images/lists/1.jpg\"}, {Login: \"john\", Name: \"John\", Icon: \"https://cdn.vuetifyjs.com/images/lists/2.jpg\"}, {Login: \"charles\", Name: \"Charles\", Icon: \"https://cdn.vuetifyjs.com/images/lists/3.jpg\"}, } loadMoreRes *vuetifyx.AutocompleteDataSource pagingRes *vuetifyx.AutocompleteDataSource ExamplePreset *presets.Builder ) func init() { db, err := gorm.Open(postgres.Open(os.Getenv(\"DB_PARAMS\")), \u0026gorm.Config{}) if err != nil { panic(err) } db.AutoMigrate(\u0026Product{}) db.Where(\"1=1\").Delete(\u0026Product{}) for i := 1; i \u003c 300; i++ { db.Create(\u0026Product{Name: fmt.Sprintf(\"Product %d\", i)}) } ExamplePreset = presets.New() ExamplePreset.URIPrefix(VuetifyAutoCompletePresetPath).DataOperator(gorm2op.DataOperator(db)) listing := ExamplePreset.Model(\u0026Product{}).Listing() loadMoreRes = listing.ConfigureAutocompleteDataSource( \u0026presets.AutocompleteDataSourceConfig{ OptionValue: \"ID\", OptionText: \"Name\", OptionIcon: func(product interface{}) string { return fmt.Sprintf(\"https://cdn.vuetifyjs.com/images/lists/%d.jpg\", product.(*Product).ID%4+1) }, KeywordColumns: []string{ \"Name\", }, PerPage: 50, }, \"loadMore\", ) pagingRes = listing.ConfigureAutocompleteDataSource( \u0026presets.AutocompleteDataSourceConfig{ OptionValue: \"ID\", OptionText: \"Name\", OptionIcon: func(product interface{}) string { return fmt.Sprintf(\"https://cdn.vuetifyjs.com/images/lists/%d.jpg\", product.(*Product).ID%4+1) }, KeywordColumns: []string{ \"Name\", }, PerPage: 20, IsPaging: true, OrderBy: \"Name\", }, \"paging\", ) } func VuetifyAutocomplete(ctx *web.EventContext) (pr web.PageResponse, err error) { pr.Body = VContainer( h.H1(\"Select many (default)\"), vuetifyx.VXAutocomplete(). Label(\"Load options from a list\"). Items(options). FieldName(\"Values1\"). ItemText(\"Name\"). ItemValue(\"Login\"), h.H1(\"Select one\"), vuetifyx.VXAutocomplete(). FieldName(\"Values2\"). Label(\"Load options from a list\"). Items(options). ItemText(\"Name\"). ItemValue(\"Login\"). Multiple(false), h.H1(\"Has icon\"), vuetifyx.VXAutocomplete(). Label(\"Load options from a list\"). Items(iconOptions). HasIcon(true), h.H1(\"Load more from remote resource\"), vuetifyx.VXAutocomplete(). FieldName(\"Values2\"). Label(\"Load options from data source\"). SetDataSource(loadMoreRes), h.H1(\"Paging with remote resource\"), vuetifyx.VXAutocomplete(). FieldName(\"Values2\"). Label(\"Load options from data source\"). SetDataSource(pagingRes), h.H1(\"Sorting\"), vuetifyx.VXAutocomplete(). FieldName(\"Values2\"). Label(\"Load options from data source\"). Sorting(true). SetDataSource(pagingRes).ChipColor(\"red\"), ) return } var VuetifyAutocompletePB = web.Page(VuetifyAutocomplete) const VuetifyAutoCompletePath = \"/samples/vuetify-auto-complete\" const VuetifyAutoCompletePresetPath = \"/samples/vuetify-auto-complete-preset\" Check the demo | Source on GitHub "},{"URL":"components-guide/composite-new-component-with-go.html","Title":"Composite new Component With Go","Body":"Any Go function that returns an htmlgo.HTMLComponent is a component,\nAny Go struct that implements MarshalHTML(ctx context.Context) ([]byte, error) function is an component.\nThey can be composite into a new component very easy. This example is ported from Bootstrap4 Navbar : 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\" Check the demo | Source on GitHub You can see from the example, We have created Navbar and Carousel components by\nsimply create Go func that returns htmlgo.HTMLComponent .\nIt is easy to pass in components as parameter, and wrap components.\nBy 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: func demoBootstrapLayout(in web.PageFunc) (out web.PageFunc) { return func(ctx *web.EventContext) (pr web.PageResponse, err error) { addGA(ctx) ctx.Injector.HeadHTML(` \u003clink rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css\" integrity=\"sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T\" crossorigin=\"anonymous\"\u003e \u003cscript src='/assets/vue.js'\u003e\u003c/script\u003e `) ctx.Injector.TailHTML(` \u003cscript src=\"https://code.jquery.com/jquery-3.3.1.slim.min.js\" integrity=\"sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo\" crossorigin=\"anonymous\"\u003e\u003c/script\u003e \u003cscript src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js\" integrity=\"sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1\" crossorigin=\"anonymous\"\u003e\u003c/script\u003e \u003cscript src=\"https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js\" integrity=\"sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM\" crossorigin=\"anonymous\"\u003e\u003c/script\u003e \u003cscript src='/assets/main.js'\u003e\u003c/script\u003e `) ctx.Injector.HeadHTML(` \u003cstyle\u003e [v-cloak] { display: none; } \u003c/style\u003e `) var innerPr web.PageResponse innerPr, err = in(ctx) if err != nil { panic(err) } pr.Body = innerPr.Body return } } You can utilize the command line tool html2go to convert existing html code to htmlgo code.\nBy 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! "},{"URL":"components-guide/integrate-a-heavy-vue-component.html","Title":"Integrate a heavy Vue Component","Body":"We can abstract any complicated of server side render component with htmlgo .\nBut a lots of components in the modern web have done many things on the client side. means there are many logic\nhappens 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\nneed to communicate with server. We are going to integrate the fantastic rich text editor tiptap to be used as any htmlgo.HTMLComponent . Step 1 : Create a @vue/cli project : $ vue create tiptapjs Modify or add a separate vue.config.js config file, const {defineConfig} = require('@vue/cli-service'); module.exports = defineConfig({ transpileDependencies: true, runtimeCompiler: true, productionSourceMap: false, devServer: { port: 3500, }, configureWebpack: { output: { libraryExport: 'default', }, externals: {vue: 'Vue'}, }, chainWebpack: config =\u003e { const svgRule = config.module.rule('svg').clear(); svgRule. test(/\\.(svg)(\\?.*)?$/). use('babel-loader'). loader('babel-loader'). end(). use('vue-svg-loader'). loader('vue-svg-loader'); }, }); 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,\nSince 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. export default { components: { EditorContent, EditorMenuBar, Icon, }, props: { value: String, }, data() { return { editor: new Editor({ content: this.$props.value, extensions: extensions(), onUpdate: ({getHTML}) =\u003e { const html = getHTML(); this.$emit(\"input\", html) }, }) } }, beforeDestroy() { this.editor.destroy() } } We injected the this.$plaid() . that is from web/corejs , Which you will need to use\nFor every Go Plaid web applications. Here we uses one function fieldValue from it.\nIt 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.\nAlso allow component user to set fieldName , which is important when posting the value to the\nserver. Step 3 : At main.js , Use a special hook to register the component to web/corejs import TipTapEditor from './editor.vue' (window.__goplaidVueComponentRegisters = window.__goplaidVueComponentRegisters || []).push((Vue) =\u003e { Vue.component('tiptap-editor', TipTapEditor) }); Step 4 : Test the component in a simple html We edited the index.html inside public to be the following: \u003chead\u003e \u003cmeta charset=\"utf-8\"\u003e \u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\"\u003e \u003cscript src=\"https://cdn.jsdelivr.net/npm/vue/dist/vue.js\"\u003e\u003c/script\u003e \u003ctitle\u003etiptapjs\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e \u003cdiv id=\"app\"\u003e \u003ctiptap-editor field-name=\"content1\" value='\u0026lt;h1\u0026gt;header\u0026lt;/h1\u0026gt;\u0026lt;p\u0026gt;abc\u0026lt;/p\u0026gt;'\u003e\u003c/tiptap-editor\u003e \u003c/div\u003e \u003cscript src=\"http://localhost:3500/app.js\"\u003e\u003c/script\u003e \u003cscript src=\"http://localhost:3100/app.js\"\u003e\u003c/script\u003e \u003c/body\u003e For http://localhost:3500/app.js to be able to serve. you have to run yarn serve in\ntiptapjs directory. http://localhost:3100/app.js is QOR5 web corejs vue project.\nSo 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\nBrowser to the index.html file, and see if your vue component can render and behave correctly. Step 5 : Use packr to pack the dist folder We write a packr box inside tiptapjs.go along side the tiptapjs folder. import ( \"embed\" \"github.com/qor5/web\" ) //go:embed tiptapjs/dist var box embed.FS func JSComponentsPack() web.ComponentsPack { v, err := box.ReadFile(\"tiptapjs/dist/tiptap.umd.min.js\") if err != nil { panic(err) } return web.ComponentsPack(v) } func CSSComponentsPack() web.ComponentsPack { v, err := box.ReadFile(\"tiptapjs/dist/tiptap.css\") if err != nil { panic(err) } return web.ComponentsPack(v) } And write a build.sh to build the javascript to production version, and run packr to pack\nthem into a_tiptap-packr.go file. CUR=$(pwd)/$(dirname $0) if test \"$1\" = 'clean'; then echo \"Removing node_modules\" rm -rf $CUR/tiptapjs/node_modules/ fi rm -r $CUR/tiptapjs/dist echo \"Building tiptapjs\" cd $CUR/tiptapjs \u0026\u0026 npm install \u0026\u0026 npm run build Step 6 : Write a Go wrapper to wrap it to be a HTMLComponent import ( \"context\" \"github.com/qor5/web\" h \"github.com/theplant/htmlgo\" ) type TipTapEditorBuilder struct { tag *h.HTMLTagBuilder } func TipTapEditor() (r *TipTapEditorBuilder) { r = \u0026TipTapEditorBuilder{ tag: h.Tag(\"tiptap-editor\"), } return } func (b *TipTapEditorBuilder) FieldName(v string) (r *TipTapEditorBuilder) { b.tag.Attr(web.VFieldName(v)...) return b } func (b *TipTapEditorBuilder) Value(v string) (r *TipTapEditorBuilder) { b.tag.Attr(\":value\", h.JSONString(v)) return b } func (b *TipTapEditorBuilder) MarshalHTML(ctx context.Context) (r []byte, err error) { return b.tag.MarshalHTML(ctx) } Step 7 : Use it in your web app To use it, first we have to mount the assets into our app mux.Handle(\"/assets/tiptap.js\", web.PacksHandler(\"text/javascript\", tiptap.JSComponentsPack(), ), ) mux.Handle(\"/assets/tiptap.css\", web.PacksHandler(\"text/css\", tiptap.CSSComponentsPack(), ), ) And reference them in our layout function. func tiptapLayout(in web.PageFunc) (out web.PageFunc) { return func(ctx *web.EventContext) (pr web.PageResponse, err error) { addGA(ctx) ctx.Injector.HeadHTML(` \u003clink rel=\"stylesheet\" href=\"/assets/tiptap.css\"\u003e \u003cscript src='/assets/vue.js'\u003e\u003c/script\u003e `) ctx.Injector.TailHTML(` \u003cscript src='/assets/tiptap.js'\u003e\u003c/script\u003e \u003cscript src='/assets/main.js'\u003e\u003c/script\u003e `) ctx.Injector.HeadHTML(` \u003cstyle\u003e [v-cloak] { display: none; } \u003c/style\u003e `) var innerPr web.PageResponse innerPr, err = in(ctx) if err != nil { panic(err) } pr.Body = innerPr.Body return } } And we write a page func to use it like any other component: 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 = ` \u003ch1\u003eHello\u003c/h1\u003e \u003cp\u003e This is a nice editor \u003c/p\u003e \u003cul\u003e \u003cli\u003e \u003cp\u003e 123 \u003c/p\u003e \u003c/li\u003e \u003cli\u003e \u003cp\u003e 456 \u003c/p\u003e \u003c/li\u003e \u003cli\u003e \u003cp\u003e 789 \u003c/p\u003e \u003c/li\u003e \u003c/ul\u003e ` } 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\" And now let's check out our fruits: Check the demo | Source on GitHub "},{"URL":"appendix/all-demo-examples.html","Title":"All Demo Examples","Body":"Vuetify List | Source Vuetify Menu | Source Presets Detail Page Top Notes | Source Presets Detail Page Details | Source Presets Detail Page Credit Cards | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Presets Hello World | Source Lazy Portals | Source Manipulate Page URL In Event Func | Source Vuetify Navigation Drawer | Source Page Func and Event Func | Source Partial Update | Source Partial Reload | Source Reload Page With a Flash | Source Switch Pages With Push State | Source The Go HTML Builder | Source Web Scope Use Locals | Source Web Scope Use PlaidForm | Source Vuetify AutoComplete | Source Vuetify Basic Inputs | Source Brand Title | Source Brand Func | Source Profile | Source Confirm Dialog | Source Presets Editing Customization Description Field | Source Presets Editing Customization File Type | Source Presets Editing Customization Tabs | Source Presets Editing Customization Validation | Source Basic filter | Source Form Handling | Source I18n | Source Vuetify LinkageSelect | Source LinkageSelect Filter Item | Source Presets Listing Customization Fields | Source Presets Listing Filters | Source Presets Listing Filter Tabs | Source Presets Listing Bulk Actions | Source Search Func | Source Presets Listing Customization Fields | Source L10n | Source Presets Detail Page Credit Cards | Source Menu Order | Source Menu Group | Source Notification Center | Source Publish | Source Shortcut | Source Vuetify Variant Sub Form | Source Worker | Source Action Worker | Source Composite New Component With Go | Source Integrate a Heavy Vue Component | Source Hello World | Source "}] +[{"URL":"index.html","Title":"Introduction","Body":"QOR5 is a Go library designed to help developers build web applications with ease and high customization. By focusing on static typing in the Go language and minimizing the need for JavaScript or TypeScript, QOR5 streamlines the development process and encourages reusability of components. In QOR5, the traditional approach of using template languages for rendering HTML is discouraged. Instead, QOR5 encourages developers to write HTML using static typing in the Go language . This design choice provides several benefits: Improved Readability and Maintainability: By using Go's static typing, you can maintain a consistent coding style throughout the entire project, making it easier to read and maintain. Better Error Checking: Static typing allows for compile-time error checking, which can help catch issues before they cause problems in production. Enhanced Reusability: QOR5 promotes the use of components, which can be easily abstracted and reused across different parts of your application. Since components are written in Go, using third-party components from other Go packages is as simple as importing and using regular Go packages. Simplified Development Process: By minimizing the need for JavaScript or TypeScript, QOR5 streamlines the development process and reduces the complexity of building interactive web applications. QOR5's approach to rendering HTML using Go's static typing eliminates the need for developers to learn and work with multiple template languages. This results in a more consistent and streamlined development experience, allowing developers to focus on the core functionality of their web applications. How is this document organized 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. Quick Sample Project: We will begin with a brief overview of a sample project, giving you a visual idea of QOR5's capabilities and functionalities. Basic Functions: In this section, we will explore the core features of QOR5, starting from listing pages to editing pages. This section covers common features found in admin websites. QOR5 Essentials and Advanced Functions: We will dive into the inner workings of QOR5, covering topics such as rendering pages and advanced features like partial page refreshing. Digging Deeper: In the final section, you will learn how to create new components for QOR5, extending its capabilities and adapting it to your specific needs. Join the Discord community : https://discord.gg/76YPsVBE4E "},{"URL":"getting-started/one-minute-quick-start.html","Title":"1 Minute Quick Start","Body":"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 : Install the command line tool with: $ go install github.com/qor5/docs/cmd/qor5@latest And run: $ qor5 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\nUse a new terminal to run source dev_env \u0026\u0026 go run main.go to start the admin app "},{"URL":"basics/listing.html","Title":"Listing","Body":"By the 1 Minute Quick Start , 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. 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\") Modify the display value To display the category name rather than category id in the post listing page. The ComponentFunc would do the work.\nThe 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. 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(\u0026c).Error; err != nil { // ignore err in the example } return h.Td(h.Text(c.Name)) }) Display virtual fields 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\")) }) DefaultScope If we want to display Post with disabled=false only. Use the Listing().Searcher to apply SQL conditions. postModelBuilder.Listing().Searcher = func(model interface{}, params *presets.SearchParams, ctx *web.EventContext) (r interface{}, totalCount int, err error){ qdb := db.Where(\"disabled != true\") return gorm2op.DataOperator(qdb).Search(model, params, ctx) } 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() rmn := postModelBuilder.Listing().RowMenu() rmn.RowMenuItem(\"Show\").ComponentFunc(func(obj interface{}, id string, ctx *web.EventContext) h.HTMLComponent { return h.Text(\"Fake Show\") }) Full Example type Post struct { ID uint Title string Body string UpdatedAt time.Time CreatedAt time.Time Disabled bool Status string 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(\u0026Post{}) 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(\"disabled != true\") 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 v.VListItem( v.VListItemIcon(v.VIcon(\"menu\")), v.VListItemTitle(h.Text(\"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(\u0026categories).Error; err != nil { // ignore err for now } return v.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(\u0026c).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(\u0026Category{}) // Use m to customize the model, Or config more models here. return } Check the demo | Source on GitHub "},{"URL":"basics/listing-customizations.html","Title":"Listing Customizations","Body":"We get a default listing page with default columns, But default columns from database\ncolumns rarely fit the needs for any real application. Change List Columns and Component of Field Here is how do we change the columns of the list and how to we change the content display of a columns. 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(\u0026comp, \"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, \u0026web.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(\u0026Company{}) comp.Editing().ValidateFunc(func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) { c := obj.(*Company) if len(c.Name) \u003c 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(\u0026comps) 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\" Check the demo | Source on GitHub 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 Filters Panel Here we continue to add filters for the list 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(\u0026Company{}).Select(\"name as text, id as value\").Scan(\u0026companyOptions).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\" Check the demo | Source on GitHub FilterDataFunc of presets.ListingBuilder setup to have the filter menu or not.\nAnd how it will combine the sql conditions when doing query. the filter menu will\nchange the url query strings with the filter values, and for date type in url query\nstring it uses unix epoch int value. So the sql condition has to convert the database\ncolumn 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 Filter Tabs Filter tabs is based on Filters configuration. But display as tabs above the list,\nYou can think it as a short cut that used very frequently to filter something instead of\nuse the pop up panel of filter. 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(\u0026c) 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\" Check the demo | Source on GitHub Query string name must be from the Filter's item configuration key field. Bulk Actions Bulk actions makes the list row show checkboxes, and you can select one or many rows,\nLater do an bulk update data for all of them. Here is how to use it: 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) \u003c 10 { ctx.Flash = \"comment should larger than 10\" return } err = db.Model(\u0026Customer{}). 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(\u0026Customer{}).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\" Check the demo | Source on GitHub 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 Search Func SearchFunc defines a data processing function for ListingBuilder .\nThis function searches for a model based on the specified search parameters.\nIt returns the search results along with the total count of matching records.\nYou can process the data displayed on the listing page here based on context or custom conditions before pagination. In the following example, the listing page only displays approved customers. func PresetsListingCustomizationSearcher(b *presets.Builder) { db := setupDB() b.URIPrefix(PresetsListingCustomizationSearcherPath).DataOperator(gorm2op.DataOperator(db)) mb := b.Model(\u0026Customer{}) mb.Listing().SearchFunc(func(model interface{}, params *presets.SearchParams, ctx *web.EventContext) (r interface{}, totalCount int, err error) { // only display approved customers qdb := db.Where(\"approved_at IS NOT NULL\") return gorm2op.DataOperator(qdb).Search(model, params, ctx) }) } Check the demo | Source on GitHub "},{"URL":"basics/filter.html","Title":"Filters","Body":"Assume we have a status filed in Post. It has 2 possible values, \"draft\" and \"online\". If we want to filter posts by its status. We can add a filter like this: 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 postBuilder := b.Model(\u0026Post{}) // get its ListingBuilder listing := postBuilder.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\"}, {Text: \"Online\", Value: \"online\"}, } return []*vuetifyx.FilterItem{ { Key: \"status\", Label: \"Status\", ItemType: vuetifyx.ItemTypeSelect, // %s is the condition. e.g. \u003e, \u003e=, =, \u003c, \u003c=, like, // ? is the value of selected option SQLCondition: `status %s ?`, Options: options, }, } }) } Check the demo | Source on GitHub QOR5 now supports 7 types of filter option. PLEASE NOTE THAT all below sample are required you to provide the SQLCondition you want to perform. 1. Filter by String Set the ItemType as vuetifyx.ItemTypeString . No Options needed.\nUnder this mode, the filter would work in 2 ways, the target value equal to the input string the target value contains the input string 2. Filter by Number Set the ItemType as vuetifyx.ItemTypeNumber . No Options needed.\nUnder this mode, the filter would work in 4 ways the target value equal to the input number the target value is between the input numbers the target value is greater than the input number the target value is less than the input number 3. Filter by Date Set the ItemType as vuetifyx.ItemTypeDate . No Options needed.\nUnder this mode, the filter would render a date picker for users to select. 4. Filter by Date Range Set the ItemType as vuetifyx.ItemTypeDateRange . No Options needed.\nUnder this mode, the filter would render 2 date pickers, \"from\" and \"to\" for users to select. 5. Filter by Datetime Range Set the ItemType as vuetifyx.ItemTypeDatetimeRange . No Options needed.\nUnder this mode, the filter would render 2 date time pickers, \"from\" and \"to\" for users to select. 6. Filter by Selectable Items Set the ItemType as vuetifyx.ItemTypeSelect . You need to provide Options like this. The Text is the text users can see in the selector, the Value is the value of the selector. Options: []*vuetifyx.SelectItem{ {Text: \"Active\", Value: \"active\"}, {Text: \"Inactive\", Value: \"inactive\"}, }, 7. Filter by Multiple Select Set the ItemType as vuetifyx.ItemTypeMultipleSelect . You need to provide Options like above \"Selectable Items\". But in this mode, the filter would render the options as multi-selectable checkboxes and the query of this filter becomes IN and NOT IN . "},{"URL":"presets-guide/editing-customizations.html","Title":"Editing","Body":"Editing an object will be always in a drawer popup. select which fields can edit for each model\nby using the .Only func of EditingBuilder , There are different ways to configure the type\nof component that is used to do the editing. Configure field for a single model Use a customized component is as simple as add the extra asset to the preset instance.\nAnd configure the component func on the field: 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\" Check the demo | Source on GitHub 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 Configure field type for all models Set a global field type to component func like the following: 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(\u0026Product{}) 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)) \u003e 0 { img = h.Img(string(val)) } var er h.HTMLComponent if len(field.Errors) \u003e 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(\u0026Product{}) mb.Editing(\"Title\", \"MainImage\") return } const PresetsEditingCustomizationFileTypePath = \"/samples/presets-editing-customization-file-type\" Check the demo | Source on GitHub 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,\nthen 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\nof how to display the edit controls, and How to save the object. Tabs Tabs can be added by using AppendTabsPanelFunc func on EditingBuilder : func PresetsEditingCustomizationTabs(b *presets.Builder) { db := setupDB() b.URIPrefix(PresetsEditingCustomizationTabsPath).DataOperator(gorm2op.DataOperator(db)) mb := b.Model(\u0026Company{}) mb.Listing(\"ID\", \"Name\") mb.Editing().AppendTabsPanelFunc(func(obj interface{}, ctx *web.EventContext) h.HTMLComponent { c := obj.(*Company) return h.Components( v.VTab(h.Text(\"New Tab\")), v.VTabItem( v.VListItemTitle(h.Text(fmt.Sprintf(\"Name: %s\", c.Name))), ).Class(\"pa-4\"), ) }) } Check the demo | Source on GitHub Validation Field level validation and display on field can be added by implement ValidateFunc ,\nand set the web.ValidationErrors result: 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) \u003c 10 { err.FieldError(\"Name\", \"name is too short\") } return }) return } const PresetsEditingCustomizationValidationPath = \"/samples/presets-editing-customization-validation\" Check the demo | Source on GitHub We validate the Name of the customer must be longer than 10 If the error happens, If will show below the field "},{"URL":"basics/brand.html","Title":"Brand","Body":"Brand refers to the top area of the left menu bar, we provide two functions BrandTitle and BrandFunc to customize it. Simple customization If you want only to change the brand string, you can use BrandTitle to set the string, the string will be displayed in the brand area with \u003cH1\u003e tag. b.URIPrefix(PresetsBrandTitlePath). BrandTitle(\"QOR5 Admin\") Check the demo | Source on GitHub Full customization When you opt-in to full brand customization, you can use BrandFunc to be responsible for drawing for the entire brand area, such as you can put your own logo image in it. b.URIPrefix(PresetsBrandFuncPath). BrandFunc(func(ctx *web.EventContext) h.HTMLComponent { return vuetify.VCardText( h.H1(\"Admin\").Style(\"color: red;\"), ).Class(\"pa-0\") }) Check the demo | Source on GitHub Profile Profile is below the brand area, where you can put the current user's information or others. We provide ProfileFunc to customize it. b.URIPrefix(PresetsProfilePath).BrandTitle(\"Admin\"). ProfileFunc(func(ctx *web.EventContext) h.HTMLComponent { // Demo logoutURL := \".\" name := \"QOR5\" account := \"hello@getqor.com\" roles := []string{\"Developer\"} return VMenu().OffsetY(true).Children( h.Template().Attr(\"v-slot:activator\", \"{on, attrs}\").Children( VList( VListItem( VListItemAvatar( VAvatar().Class(\"ml-1\").Color(\"secondary\").Size(40).Children( h.Span(string(name[0])).Class(\"white--text text-h5\"), ), ), VListItemContent( VListItemTitle(h.Text(name)), h.Br(), VListItemSubtitle(h.Text(strings.Join(roles, \", \"))), ), ).Class(\"pa-0 mb-2\"), VListItem( VListItemContent( VListItemTitle(h.Text(account)), ), VListItemIcon( VIcon(\"logout\").Small(true).Attr(\"@click\", web.Plaid().URL(logoutURL).Go()), ), ).Class(\"pa-0 my-n4 ml-1\").Dense(true), ).Class(\"pa-0 ma-n4\"), ), ) }) Check the demo | Source on GitHub "},{"URL":"basics/menu.html","Title":"Menu","Body":"Menu refers to the list on the left side of the page, such as the menu of the Demo below contains Customers and Companies. Check the demo | Source on GitHub Menu order Sorting menus is very simple, use MenuOrder to sort menus as you want by slug name . b.URIPrefix(PresetsMenuOrderPath). MenuOrder( \"books\", \"videos\", \"musics\", ) Check the demo | Source on GitHub Menu group and icon MenuGroup can merge multiple items into one group, as shown in the following code. Use MenuIcon on ModelBuilder can set the item icon, and set menu group icon by Icon following MenuGroup . Icon strings can be found at https://fonts.google.com/icons . mb := b.Model(\u0026book{}).MenuIcon(\"book\") mb.Listing().PageFunc(func(ctx *web.EventContext) (r web.PageResponse, err error) { r.Body = vuetify.VContainer( h.Div( h.H1(\"book\"), ).Class(\"text-center mt-8\"), ) return }) b.MenuOrder( \"books\", b.MenuGroup(\"Media\").SubItems( \"videos\", \"musics\", ).Icon(\"perm_media\"), ) Check the demo | Source on GitHub "},{"URL":"presets-guide/detail-page-for-complex-object.html","Title":"Detailing","Body":"By default, presets will only generate the listing page, editing page for a model,\nIt's for simple objects. But for a complicated object with a lots of relationships and connections,\nand as the main data model of your system, It's better to have detail page for them. In there\nYou can add all kinds of operations conveniently. 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(\u0026Note{}) 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(\u0026notes).Error if err != nil { panic(err) } dt := vx.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 vx.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(\u0026Note{}). 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\" Check the demo | Source on GitHub The name of detailing fields are just a place holder for decide ordering CellComponentFunc customize how the cell display vx.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 vx.Card display a card with toolbar you can setup action buttons We reference the new form drawer that b.Model(\u0026Note{}) creates, but hide notes in the menu Details Info components and actions A vx.DetailInfo component is used for display main detail field of the model.\nAnd you can add any actions to the detail page with ease: 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(\u0026CreditCard{}) 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 := vx.DetailInfo( vx.DetailColumn( vx.DetailField(vx.OptionalText(cu.Name).ZeroLabel(\"No Name\")).Label(\"Name\"), vx.DetailField(vx.OptionalText(cu.Email).ZeroLabel(\"No Email\")).Label(\"Email\"), vx.DetailField(vx.OptionalText(cusID).ZeroLabel(\"No ID\")).Label(\"ID\"), vx.DetailField(vx.OptionalText(cu.CreatedAt.Format(\"Jan 02,15:04 PM\")).ZeroLabel(\"\")).Label(\"Created\"), vx.DetailField(vx.OptionalText(termAgreed).ZeroLabel(\"Not Agreed Yet\")).Label(\"Terms Agreed\"), ).Header(\"ACCOUNT INFORMATION\"), vx.DetailColumn( vx.DetailField(h.RawHTML(cu.Description)).Label(\"Description\"), ).Header(\"DETAILS\"), ) return vx.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(id string, ctx *web.EventContext) (err error) { if ctx.R.FormValue(\"Agree\") != \"true\" { ve := \u0026web.ValidationErrors{} ve.GlobalError(\"You must agree the terms\") err = ve return } err = db.Model(\u0026Customer{}).Where(\"id = ?\", id). Updates(map[string]interface{}{\"term_agreed_at\": time.Now()}).Error return }).ComponentFunc(func(id 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\" Check the demo | Source on GitHub 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 More Usage for Data Table A vx.DataTable component is very featured rich, Here check out the row expandable example: 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(\u0026CreditCard{}) 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(\u0026cards).Error if err != nil { panic(err) } dt := vx.DataTable(cards). WithoutHeader(true). RowExpandFunc(func(obj interface{}, ctx *web.EventContext) h.HTMLComponent { card := obj.(*CreditCard) return vx.DetailInfo( vx.DetailColumn( vx.DetailField(vx.OptionalText(card.Name).ZeroLabel(\"No Name\")).Label(\"Name\"), vx.DetailField(vx.OptionalText(card.Number).ZeroLabel(\"No Number\")).Label(\"Number\"), vx.DetailField(vx.OptionalText(card.ExpireYearMonth).ZeroLabel(\"No Expires\")).Label(\"Expires\"), vx.DetailField(vx.OptionalText(card.Type).ZeroLabel(\"No Type\")).Label(\"Type\"), vx.DetailField(vx.OptionalText(card.Phone).ZeroLabel(\"No phone provided\")).Label(\"Phone\"), vx.DetailField(vx.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 vx.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(\u0026CreditCard{}). 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\" Check the demo | Source on GitHub RowExpandFunc config the content when data table row expand cc.Editing setup the fields when edit cc.Creating setup the fields when create "},{"URL":"basics/layout.html","Title":"Layout","Body":"Presets comes with a built-in layout that works out of the box. And there are some ways to customzie the layout/theme. Theme Presets UI is based on Vuetify , you can modify the Admin theme by configuring the Vuetify options presetsBuilder.VuetifyOptions(` { icons: { iconfont: 'md', }, theme: { themes: { light: { primary: \"#673ab7\", secondary: \"#009688\", accent: \"#ff5722\", error: \"#f44336\", warning: \"#ff9800\", info: \"#8bc34a\", success: \"#4caf50\" }, }, }, } `) Assets If you need third-party front-end libraries to achieve some functions,\nyou can inject them via the ExtraAsset method, and they will be automatically served. presetsBuilder.ExtraAsset(\"/redactor.js\", \"text/javascript\", richeditor.JSComponentsPack()) presetsBuilder.ExtraAsset(\"/redactor.css\", \"text/css\", richeditor.CSSComponentsPack()) you can also call Injector in AssetFunc to add meta, add custom HTML in HEAD and TAIL. presetsBuilder.AssetFunc(func(ctx *web.EventContext) { ctx.Injector.Meta(web.MetaKey(\"charset\"), \"charset\", \"utf8\") ctx.Injector.HeadHTML(`\u003cscript src=\"https://cdn.example.com/hello.js\"\u003e\u003c/script\u003e`) }) Layout You can change the entire layout via LayoutFunc . The default layout is https://github.com/qor5/admin/blob/1e97c0dd45615fb7593245575ab0fea4f98c58b3/presets/presets.go#L860-L969 Layout Options We also provide some options to tweak the layout modelBuilder.LayoutConfig(\u0026presets.LayoutConfig{ SearchBoxInvisible: true, NotificationCenterInvisible: true, }) Plain Layout And We provide PlainLayout which has no UI content except necessary assets.\nIt will be helpful when there are some pages completely independent of Presets layout but still need to be consistent with the Presets theme. "},{"URL":"basics/login.html","Title":"Login","Body":"Login package provides comprehensive login authentication logic and related UI interfaces. It is designed to simplify the process of adding user authentication to QOR5-based backend development project. In QOR5 admin development, we recommend using github.com/qor5/admin/login , which wraps github.com/qor5/x/login to keep the theme of login UI consistent with Presets and provide more powerful features. Basic Usage The example shows how to enable both username/password login and OAuth login. import ( \"net/http\" \"os\" \"github.com/markbates/goth/providers/github\" \"github.com/markbates/goth/providers/google\" plogin \"github.com/qor5/admin/login\" \"github.com/qor5/admin/presets\" . \"github.com/qor5/ui/vuetify\" \"github.com/qor5/web\" \"github.com/qor5/x/login\" . \"github.com/theplant/htmlgo\" \"gorm.io/gorm\" ) type User struct { gorm.Model Name string Address string login.UserPass login.OAuthInfo login.SessionSecure } func serve() { pb := presets.New() lb := plogin.New(pb). DB(DB). UserModel(\u0026User{}). Secret(os.Getenv(\"LOGIN_SECRET\")). OAuthProviders( \u0026login.Provider{ Goth: google.New(os.Getenv(\"LOGIN_GOOGLE_KEY\"), os.Getenv(\"LOGIN_GOOGLE_SECRET\"), os.Getenv(\"BASE_URL\")+\"/auth/callback?provider=google\"), Key: \"google\", Text: \"Google\", }, \u0026login.Provider{ Goth: github.New(os.Getenv(\"LOGIN_GITHUB_KEY\"), os.Getenv(\"LOGIN_GITHUB_SECRET\"), os.Getenv(\"BASE_URL\")+\"/auth/callback?provider=github\"), Key: \"github\", Text: \"Login with Github\", }, ) pb.ProfileFunc(func(ctx *web.EventContext) HTMLComponent { return A(Text(\"logout\")).Href(lb.LogoutURL) }) r := http.NewServeMux() r.Handle(\"/\", pb) lb.Mount(r) mux := http.NewServeMux() mux.Handle(\"/\", lb.Middleware()(r)) http.ListenAndServe(\":8080\", nil) } Username/Password Login To enable Username/Password login, the UserModel needs to implement the UserPasser interface. There is a default implementation - UserPass . type User struct { gorm.Model login.UserPass } Change Password There are three ways to change the password: 1. Visit the default change password page. 2. Call the OpenChangePasswordDialogEvent event to change it in dialog. VBtn(\"Change Password\").OnClick(plogin.OpenChangePasswordDialogEvent) 3. Change the password directly in Editing. userModelBuilder.Editing().Field(\"Password\"). SetterFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) (err error) { u := obj.(*User) if v := ctx.R.FormValue(field.Name); v != \"\" { u.Password = v u.EncryptPassword() } return nil }) MaxRetryCount By default, it allows 5 login attempts with incorrect credentials, and if the limit is exceeded, the user will be locked for 1 hour. This helps to prevent brute-force attacks on the login system. You can call MaxRetryCount to set the maximum retry count. If you set MaxRetryCount to a value less than or equal to 0, it means there is no limit of login attempts, and the user will not be locked after a certain number of failed login attempts. loginBuilder.MaxRetryCount(count) TOTP There is TOTP (Time-based One-time Password) functionality out of the box, which is enabled by default. loginBuilder.TOTP(enable, login.TOTPConfig{ Issuer: \"Issuer\", }) Google reCAPTCHA Google reCAPTCHA is disabled by default. loginBuilder.Recaptcha(enable, login.RecaptchaConfig{ SiteKey: \"SiteKey\", SecretKey: \"SecretKey\", }) OAuth Login OAuth login is based on goth . OAuth login does not require a UserModel . If there is a UserModel , it needs to implement the OAuthUser interface. There is a default implementation - OAuthInfo . type User struct { gorm.Model login.OAuthInfo } Session Secure The SessionSecurer provides a way to manage unique salt for a user record. There is a default implementation - SessionSecure . type User struct { gorm.Model login.UserPass login.OAuthInfo login.SessionSecure } SessionSecurer helps to ensure user security even in the event of secret leakage. When a user logs in, SessionSecurer generates a random salt and associates it with the user's record. This salt is then used to sign the user's session token. When the user makes requests to the server, the server verifies that the session token has been signed with the correct salt. If the salt has been changed, the session token is considered invalid and the user is logged out. Hooks Hooks are functions that are called before or after certain events. The following hooks are available: BeforeSetPassword Extra Values password This hook is called before resetting or changing a password. The hook can be used to validate password formats. AfterLogin This hook is called after a successful login. AfterFailedToLogin Extra Values login error This hook is called after a failed login. Note that the user parameter may be nil. AfterUserLocked This hook is called after a user is locked. AfterLogout This hook is called after a logout. AfterConfirmSendResetPasswordLink Extra Values reset link This hook is called after confirming the sending of a password reset link. This is where the code to send the reset link to the user should be written. AfterResetPassword This hook is called after a password is reset. AfterChangePassword This hook is called after a password is changed. AfterExtendSession Extra Values old session token This hook is called after a session is extended. AfterTOTPCodeReused This hook is called after a TOTP code has been reused. AfterOAuthComplete This hook is called after an OAuth authentication is completed. Customize Pages To customize pages, there are two ways: 1. Each page has a corresponding xxxPageFunc to rewrite the page content. You can easily customize a page by copying the default page func and modifying it according to your needs. loginBuilder.LoginPageFunc(func(ctx *web.EventContext) (r web.PageResponse, err error) { r.Body = Text(\"This is login page\") return }) 2. Only mount the API and serve the login pages manually. When you want to embed the login form into an existing page, this way can be very useful. loginBuilder.LoginPageURL(\"/custom-login-page\") loginBuilder.MountAPI(mux) mux.Handle(\"/custom-login-page\", loginPage) "},{"URL":"presets-guide/permissions.html","Title":"Permissions","Body":"QOR5 permission is based on https://github.com/ory/ladon . A piece of policy looks like this: Who is able to do what on something (with given some context ) perm.PolicyFor(Who).WhoAre(Able).ToDo(What).On(Something).Given(Context) Who - Subject Typically in admin system, they are roles like Admin , Super Admin . Use SubjectsFunc to fetch current subjects: permBuilder.SubjectsFunc(func(r *http.Request) []string { return subjects_like_user_roles }) Able - Effect perm.Allowed perm.Denied What - Action presets has a list of actions: presets.PermList presets.PermGet presets.PermCreate presets.PermUpdate presets.PermDelete And you can define other specific actions if needed. Something - Resource An arbitrary unique resource name. The presets builtin resource format is :presets:mg_menu_group:uri:resource_rn:f_field: . For example :presets:user_management:users:1: represents the user record with id 1 under uri user_management. Use * as wildcard. Context - Condition Optional. The current context that containing condition information about the resource. Use ContextFunc to set the context: permBuilder.ContextFunc(func(r *http.Request, objs []interface{}) perm.Context { c := make(perm.Context) for _, obj := range objs { switch v := obj.(type) { case resource1: c[\"owner\"] = v.Owner // ...other resource cases } } return c }) Policy uses Given to set conditions: perm.PolicyFor(Who).WhoAre(Able).ToDo(What).On(\"*:resource1:*\").Given(perm.Conditions{ \"owner\": \u0026ladon.EqualsSubjectCondition{}, }) Custom Action Let's say there is a button on User detailing page used to ban the user. And only super_admin users have permission to execute this action. First, create a verifier verifier := perm.NewVerifier(\"module_users\", presetsBuilder.GetPermission()) Then inject this verifier to relevant logic, such as whether to show the ban button. validate permission before execute the ban action. if verifier.Do(\"ban\").ObjectOn(user).WithReq(r).IsAllowed() == nil { // ui: show the ban button // action: can execute the ban action } Finally, add policy perm.PolicyFor(\"super_admin\").WhoAre(perm.Allowed).ToDo(\"ban\").On(\":module_users:*\") Example presetsBuilder.Permission( perm.New().Policies( // admin can do anything perm.PolicyFor(\"admin\").WhoAre(perm.Allowed).ToDo(perm.Anything).On(perm.Anything), // viewer can view anything except users perm.PolicyFor(\"viewer\").WhoAre(perm.Allowed).ToDo(presets.PermRead...).On(perm.Anything), perm.PolicyFor(\"viewer\").WhoAre(perm.Denied).ToDo(perm.Anything).On(\"*:users:*\"), // editor can edit their own articles perm.PolicyFor(\"editor\").WhoAre(perm.Allowed).ToDo(perm.Anything).On(\"*:articles:*\").Given(perm.Conditions{ \"owner_id\": \u0026ladon.EqualsSubjectCondition{}, }), ).SubjectsFunc(func(r *http.Request) (ss []string) { user := getCurrentUser(r) ss = append(ss, user.ID) ss = append(ss, user.Roles...) return ss }).ContextFunc(func(r *http.Request, objs []interface{}) perm.Context { c := make(perm.Context) for _, obj := range objs { switch v := obj.(type) { case *Article: c[\"owner_id\"] = v.OwnerID } } return c }), ) Debug perm.Verbose = true prints permission logs which is very helpful for debugging the permission policies: have permission: true, req: \u0026ladon.Request{Resource:\":presets:articles:\", Action:\"presets:list\", Subject:\"viewer\", Context:ladon.Context(nil)} have permission: true, req: \u0026ladon.Request{Resource:\":presets:articles:articles:1:\", Action:\"presets:update\", Subject:\"viewer\", Context:ladon.Context(nil)} have permission: false, req: \u0026ladon.Request{Resource:\":presets:articles:articles:2:\", Action:\"presets:update\", Subject:\"viewer\", Context:ladon.Context(nil)} "},{"URL":"presets-guide/role.html","Title":"Role","Body":"Role provides a UI interface to manage roles(subjects) and their permissions. 1. enable permission DBPolicy perm.New(). Policies( // static policies ). DBPolicy(perm.NewDBPolicy(db)) 2. configure role set resources that you want to manage on interface rb := role.New(db). Resources([]*vuetify.DefaultOptionItem{ {Text: \"All\", Value: \"*\"}, {Text: \"Posts\", Value: \"*:posts:*\"}, {Text: \"Customers\", Value: \"*:customers:*\"}, {Text: \"Products\", Value: \"*:products:*\"}, }) (optional) set actions, the default value is the following // default value rb.Actions([]*vuetify.DefaultOptionItem{ {Text: \"All\", Value: \"*\"}, {Text: \"List\", Value: presets.PermList}, {Text: \"Get\", Value: presets.PermGet}, {Text: \"Create\", Value: presets.PermCreate}, {Text: \"Update\", Value: presets.PermUpdate}, {Text: \"Delete\", Value: presets.PermDelete}, }) (optional) set editor subject to set who can edit Role rb.EditorSubject(\"RoleEditor\") attach role to presets builder rb.Configure(presetsBuilder) "},{"URL":"basics/notification-center.html","Title":"Notification Center","Body":"To enable notification center: Call NotificationFunc on presets.Builder With 2 function parameters\nlike this builder.NotificationFunc(NotifierComponent(), NotifierCount()) The first function is for rendering the content of the popup after user clicked the \"bell icon\".\nThe second function is for rendering the number at the top right corner of the \"bell icon\". import ( \"github.com/qor5/admin/presets\" \"github.com/qor5/admin/presets/gorm2op\" \"github.com/qor5/docs/docsrc/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(\u0026utils.Page{}) b.Model(\u0026utils.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 } } Check the demo | Source on GitHub "},{"URL":"basics/shortcut.html","Title":"Keyboard Shortcut","Body":"To add keyboard shortcut to a button: Trigger the event by GlobalEvents .\nYou 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.\nIn the example, the event would only be triggered when locals.shortCutEnabled is opened. 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) =\u003e 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\" Check the demo | Source on GitHub "},{"URL":"basics/confirm-dialog.html","Title":"Confirm Dialog","Body":"presets.OpenConfirmDialog is a pre-defined event used to show a confirm dialog for user to do confirm before executing the actual action. Queries presets.ConfirmDialogConfirmEvent required Usually the value will be web.Plaid().EventFunc(the actual action event)....Go() . presets.ConfirmDialogPromptText optional To customize the prompt text. presets.ConfirmDialogDialogPortalName optional To use a custom portal for dialog. Example vuetify.VBtn(\"Delete File\"). Attr(\"@click\", web.Plaid(). EventFunc(presets.OpenConfirmDialog). Query(presets.ConfirmDialogConfirmEvent, `alert(\"file deleted\")`, ). Go(), ), Check the demo | Source on GitHub "},{"URL":"slug.html","Title":"Slug","Body":"Slug provides an easy way to create pretty URLs for your model. Usage If the source field called Name , Use *WithSlug which is NameWithSlug as the slug field name, the field type should be slug.Slug . Then the pretty URL would be derived from Name automatically on editing. type User struct {\n\tgorm.Model\n\tName string\n\tNameWithSlug slug.Slug\n} "},{"URL":"seo.html","Title":"SEO","Body":"The SEO library facilitates the optimization of Search Engine results by managing and injecting dynamic data into HTML tags. Usage Initialize a Collection instance. The Collection manages all the registered models and hold global seo settings collection := seo.NewCollection()\n\n// Turn off the default inherit the upper level SEO data when the current SEO data is missing\ncollection.SetInherited(false) Register models to SEO // Register mutiple SEO by name\ncollection.RegisterSEOByNames(\"Product\", \"Announcement\")\n\n// Register a SEO by model\ntype Product struct{\n\tName string\n\tSetting Setting\n}\ncollection.RegisterSEO(\u0026Product{}) Remove models from SEO // Remove by struct\ncollection.RemoveSEO(\u0026Product{})\n// Remove by name\ncollection.RemoveSEO(\"Not Found\") Configuration Change the default global SEO name collection.SetGlobalName(\"My Global SEO\") Change the default context db key collection.SetDBContextKey(\"My DB\") Change the default SEO name collection.RegisterSEO(\u0026Product{}).SetName(\"My Product\") Register customized variables collection.RegisterSEO(\u0026Product{}).\n\tRegisterContextVariables(\"og:image\", func(obj interface{}, _ *Setting, _ *http.Request) string {\n\t\t// this will render \"og:image\" with the value of the object in the current request\n\t\treturn obj.image.url\n\t}).\n\tRegisterContextVariables(\"Name\", func(obj interface{}, _ *Setting, _ *http.Request) string {\n\t\treturn obj.Name\n\t}) Register setting variable This variable will be saved in the database and available as a global variable while editing SEO settings. collection.RegisterSEO(\u0026Product{}).RegisterSettingVaribles(struct{ProductTag string}{}) Render SEO html data // Render Global SEO\ncollection.RenderGlobal(request)\n\n// Render SEO by name\ncollection.Render(\"product\", request)\n\n// Render SEO by model\ncollection.Render(Product{}, request) Customization You can customize your SEO settings by implementing the interface and adding functions such as l10n and publish. type QorSEOSettingInterface interface { GetName() string SetName(string) GetSEOSetting() Setting SetSEOSetting(Setting) GetVariables() Variables SetVariables(Variables) GetLocale() string SetLocale(string) GetTitle() string GetDescription() string GetKeywords() string GetOpenGraphTitle() string GetOpenGraphDescription() string GetOpenGraphURL() string GetOpenGraphType() string GetOpenGraphImageURL() string GetOpenGraphImageFromMediaLibrary() media_library.MediaBox GetOpenGraphMetadata() []OpenGraphMetadata } Suppose MySEOSetting implemented the above interface type MySEOSetting struct{\n\t\tQorSEOSetting\n\t\t// publish\n\t\t// l10n\n} Use SetSettingModel function to set it collection.SetSettingModel(\u0026MySEOSetting{}) Example var SeoCollection *seo.Collection func ConfigureSeo(b *presets.Builder, db *gorm.DB) { SeoCollection = seo.NewCollection() SeoCollection.RegisterSEO(\u0026models.Post{}).RegisterContextVariables( \"Title\", func(object interface{}, _ *seo.Setting, _ *http.Request) string { if article, ok := object.(models.Post); ok { return article.Title } return \"\" }, ).RegisterSettingVaribles(struct{ Test string }{}) SeoCollection.RegisterSEOByNames(\"Product\", \"Announcement\") SeoCollection.Configure(b, db) } Definition Collection manages all the registered models and hold global seo settings. type Collection struct { registeredSEO []*SEO globalName string //default name is GlobalSEO inherited bool //default is true. the order is model seo setting, system seo setting, global seo setting dbContextKey interface{} // get db from context settingModel interface{} // db model afterSave func(ctx context.Context, settingName string, locale string) error // hook called after saving } SEO provides system-level default page matadata. type SEO struct { name string modelTyp reflect.Type contextVariables map[string]contextVariablesFunc // fetch context variables from request settingVariables interface{} // fetch setting variables from db } You can use seo setting at the model level, but you need to register the model to the system SEO type Product struct { Name string SEO Setting } collection.RegisterSEO(\u0026Product{}) "},{"URL":"activity-log.html","Title":"Activity Log","Body":"QOR5 provides a built-in activity module for recording model operations that may be important for admin users of CMS. These records are designed to be easily queried and audited, and the activity module supports the following features: Detailed change logging functionality for model data modifications. Allow certain fields to be ignored when comparing modified data, such as the update time. Customization of the diffing process for complex field types, like time.Time. Customization of the keys used to identify model data. Support both automatic and manual CRUD operation recording. Provide flexibility to customize the actions other than default CRUD. An page for querying the activity log via QOR5 admin Initialize the activity package To initialize activity package with the default configuration, you need to pass a presets.Builder instance and a database instance. presetsBuilder := presets.New() db, err := gorm.Open(postgres.Open(os.Getenv(\"DB_PARAMS\")), \u0026gorm.Config{}) if err != nil { panic(err) } activityBuilder := activity.New(presetsBuilder, db) By default, the activity package uses QOR5 login package's login.UserKey as the default key to fetch the current user from the context. If you want to use your own key, you can use the SetCreatorContextKey function. Same with above, the activity package uses the db instance that passed in during initialization to perform db operations. If you need another db to do the work, you can use SetDBContextKey method. Register the models that require activity tracking This example demonstrates how to register Product into the activity. The activities on the product model will be automatically recorded when it is created, updated, or deleted. type Product struct { Title string Code string Price float64 } productModel := presetsBuilder.Model(\u0026Product{}) activityBuilder.RegisterModel(productModel).EnableActivityInfoTab().AddKeys(\"Title\").AddIgnoredFields(\"Code\").SkipDelete() By default, the activity package will use the primary key as the key to indentify the current model data. You can use SetKeys and AddKeys methods to customize it. When diffing the modified data, the activity package will ignore the ID , CreatedAt , UpdatedAt , DeletedAt fields. You can either use AddIgnoredFields to append your own fields to the default ignored fields. Or SetIgnoredFields method to replace the default ignored fields. For special fields like time.Time or media files handled by QOR5 media_library, activity package already handled them. You can use AddTypeHanders method to handle your own field types. If you want to skip the automatic recording, you can use SkipCreate , SkipUpdate and SkipDelete methods. The Activity package allows for displaying the activities of a record on its editing page. Simply use the EnableActivityInfoTab method to enable this feature. Once enabled, you can customize the format of each activity's display text using the SetTabHeading method. Additionally, you can make each activity a link to the corresponding record using the SetLink method. Record the activity log manually If you register a preset model into the activity, the activity package will automatically record the activity log for CRUD operations. However, if you need to manually record the activity log for other operations or if you want to register a non-preset model, you can use the following sample code. currentCtx := context.WithValue(context.Background(), activity.CreatorContextKey, \"user1\") activityBuilder.AddRecords(\"Publish\", currentCtx, \u0026Product{Title: \"Product 1\", Code: \"P1\", Price: 100}) activityBuilder.AddRecords(\"Update Price\", currentCtx, \u0026Product{Title: \"Product 1\", Code: \"P1\", Price: 200}) "},{"URL":"basics/worker.html","Title":"Worker","Body":"Worker runs a single Job in the background, it can do so immediately or at a scheduled time. Once registered with QOR Admin, Worker will provide a Workers section in the navigation tree, containing pages for listing and managing the following aspects of Workers: All Jobs. Running: Jobs that are currently running. Scheduled: Jobs which have been scheduled to run at a time in the future. Done: finished Jobs. Errors: any errors output from any Workers that have been run. Note The default que GoQueQueue( https://github.com/tnclong/go-que ) only supports postgres for now. To make a job abortable, you need to check ctx.Done() channel in job handler and stop the handler func. Example import ( \"context\" \"errors\" \"fmt\" \"time\" \"github.com/qor5/admin/presets\" \"github.com/qor5/admin/worker\" ) func MountWorker(b *presets.Builder) { wb := worker.New(DB) wb.Configure(b) defer wb.Listen() addJobs(wb) } func addJobs(w *worker.Builder) { w.NewJob(\"noArgJob\"). Handler(func(ctx context.Context, job worker.QorJobInterface) error { job.AddLog(\"hoho1\") job.AddLog(\"hoho2\") job.AddLog(\"hoho3\") return nil }) type ArgJobResource struct { F1 string F2 int F3 bool } argJb := w.NewJob(\"argJob\"). Resource(\u0026ArgJobResource{}). Handler(func(ctx context.Context, job worker.QorJobInterface) error { jobInfo, _ := job.GetJobInfo() job.AddLog(fmt.Sprintf(\"Argument %#+v\", jobInfo.Argument)) return nil }) // you can to customize the resource Editing via GetResourceBuilder() argJb.GetResourceBuilder().Editing() w.NewJob(\"progressTextJob\"). Handler(func(ctx context.Context, job worker.QorJobInterface) error { job.AddLog(\"hoho1\") job.AddLog(\"hoho2\") job.AddLog(\"hoho3\") job.SetProgressText(`\u003ca href=\"https://www.google.com\"\u003eDownload users\u003c/a\u003e`) return nil }) // check ctx.Done() to stop the handler w.NewJob(\"longRunningJob\"). Handler(func(ctx context.Context, job worker.QorJobInterface) error { for i := 1; i \u003c= 5; i++ { select { case \u003c-ctx.Done(): job.AddLog(\"job aborted\") return nil default: job.AddLog(fmt.Sprintf(\"%v\", i)) job.SetProgress(uint(i * 20)) time.Sleep(time.Second) } } return nil }) // insert worker.Schedule to resource to make a job schedulable type ScheduleJobResource struct { F1 string worker.Schedule } w.NewJob(\"scheduleJob\"). Resource(\u0026ScheduleJobResource{}). Handler(func(ctx context.Context, job worker.QorJobInterface) error { jobInfo, _ := job.GetJobInfo() job.AddLog(fmt.Sprintf(\"%#+v\", jobInfo.Argument)) return nil }) w.NewJob(\"errorJob\"). Handler(func(ctx context.Context, job worker.QorJobInterface) error { job.AddLog(\"=====perform error job\") return errors.New(\"imError\") }) w.NewJob(\"panicJob\"). Handler(func(ctx context.Context, job worker.QorJobInterface) error { job.AddLog(\"=====perform panic job\") panic(\"letsPanic\") }) } Check the demo | Source on GitHub Action Worker Action Worker is used to visualize the progress of long-running actions. import ( \"context\" \"fmt\" \"time\" \"github.com/qor5/admin/presets\" \"github.com/qor5/admin/worker\" \"github.com/qor5/ui/vuetify\" \"github.com/qor5/web\" h \"github.com/theplant/htmlgo\" \"gorm.io/gorm\" ) type ExampleResource struct { gorm.Model Name string } func MountActionWorker(b *presets.Builder) { mb := b.Model(\u0026ExampleResource{}) mb.Listing().ActionsAsMenu(true) wb := worker.New(DB) wb.Configure(b) defer wb.Listen() addActionJobs(mb, wb) } func addActionJobs(mb *presets.ModelBuilder, wb *worker.Builder) { lb := mb.Listing() noParametersJob := wb.ActionJob( \"No parameters\", mb, func(ctx context.Context, job worker.QorJobInterface) error { for i := 1; i \u003c= 10; i++ { select { case \u003c-ctx.Done(): job.AddLog(\"job aborted\") return nil default: job.SetProgress(uint(i * 10)) time.Sleep(time.Second) } } job.SetProgressText(`\u003ca href=\"https://qor5-test.s3.ap-northeast-1.amazonaws.com/system/media_libraries/37/file.@qor_preview.png\"\u003ePlease download this file\u003c/a\u003e`) return nil }, ).Description(\"This test demo is used to show that an no parameter job can be executed\") parametersBoxJob := wb.ActionJob( \"Parameter input box\", mb, func(ctx context.Context, job worker.QorJobInterface) error { for i := 1; i \u003c= 10; i++ { select { case \u003c-ctx.Done(): job.AddLog(\"job aborted\") return nil default: job.SetProgress(uint(i * 10)) time.Sleep(time.Second) } } job.SetProgressText(`\u003ca href=\"https://qor5-test.s3.ap-northeast-1.amazonaws.com/system/media_libraries/37/file.@qor_preview.png\"\u003ePlease download this file\u003c/a\u003e`) return nil }, ).Description(\"This test demo is used to show that an input box when there are parameters\"). Params(\u0026struct{ Name string }{}) displayLogJob := wb.ActionJob( \"Display log\", mb, func(ctx context.Context, job worker.QorJobInterface) error { for i := 1; i \u003c= 10; i++ { select { case \u003c-ctx.Done(): job.AddLog(\"job aborted\") return nil default: job.SetProgress(uint(i * 10)) job.AddLog(fmt.Sprintf(\"%v\", i)) time.Sleep(time.Second) } } job.SetProgressText(`\u003ca href=\"https://qor5-test.s3.ap-northeast-1.amazonaws.com/system/media_libraries/37/file.@qor_preview.png\"\u003ePlease download this file\u003c/a\u003e`) return nil }, ).Description(\"This test demo is used to show the log section of this job\"). Params(\u0026struct{ Name string }{}). DisplayLog(true). ProgressingInterval(4000) getArgsJob := wb.ActionJob( \"Get Args\", mb, func(ctx context.Context, job worker.QorJobInterface) error { jobInfo, err := job.GetJobInfo() if err != nil { return err } job.AddLog(fmt.Sprintf(\"Action Params Name is %#+v\", jobInfo.Argument.(*struct{ Name string }).Name)) job.AddLog(fmt.Sprintf(\"Origina Context AuthInfo is %#+v\", jobInfo.Context[\"AuthInfo\"])) job.AddLog(fmt.Sprintf(\"Origina Context URL is %#+v\", jobInfo.Context[\"URL\"])) for i := 1; i \u003c= 10; i++ { select { case \u003c-ctx.Done(): return nil default: job.SetProgress(uint(i * 10)) time.Sleep(time.Second) } } job.SetProgressText(`\u003ca href=\"https://qor5-test.s3.ap-northeast-1.amazonaws.com/system/media_libraries/37/file.@qor_preview.png\"\u003ePlease download this file\u003c/a\u003e`) return nil }, ).Description(\"This test demo is used to show how to get the action's arguments and original page context\"). Params(\u0026struct{ Name string }{}). DisplayLog(true). ContextHandler(func(ctx *web.EventContext) map[string]interface{} { auth, err := ctx.R.Cookie(\"auth\") if err == nil { return map[string]interface{}{\"AuthInfo\": auth.Value} } return nil }) lb.Action(\"Action Job - No parameters\"). ButtonCompFunc( func(ctx *web.EventContext) h.HTMLComponent { return vuetify.VBtn(\"Action Job - No parameters\").Color(\"secondary\").Depressed(true).Class(\"ml-2\"). Attr(\"@click\", noParametersJob.URL()) }) lb.Action(\"Action Job - Parameter input box\"). ButtonCompFunc( func(ctx *web.EventContext) h.HTMLComponent { return vuetify.VBtn(\"Action Job - Parameter input box\").Color(\"secondary\").Depressed(true).Class(\"ml-2\"). Attr(\"@click\", parametersBoxJob.URL()) }) lb.Action(\"Action Job - Display log\"). ButtonCompFunc( func(ctx *web.EventContext) h.HTMLComponent { return vuetify.VBtn(\"Action Job - Display log\").Color(\"secondary\").Depressed(true).Class(\"ml-2\"). Attr(\"@click\", displayLogJob.URL()) }) lb.Action(\"Action Job - Get Args\"). ButtonCompFunc( func(ctx *web.EventContext) h.HTMLComponent { return vuetify.VBtn(\"Action Job - Get Args\").Color(\"secondary\").Depressed(true).Class(\"ml-2\"). Attr(\"@click\", getArgsJob.URL()) }) } Check the demo | Source on GitHub "},{"URL":"basics/publish.html","Title":"Publish","Body":"Publish controls the online/offline status of records. It generalizes publishing using 3 main modules: Status : to flag a record be online/offline Schedule : to schedule records to be online/offline automatically Version : to allow a record to have more than one copies and chain them together Usage Inject modules to the resource model. type Product struct { gorm.Model Name string Price int publish.Status publish.Schedule publish.Version } Implement primary slug interfaces for passing the values of primary keys between events var _ presets.SlugEncoder = (*Product)(nil) var _ presets.SlugDecoder = (*Product)(nil) func (p *Product) PrimarySlug() string { return fmt.Sprintf(\"%v_%v\", p.ID, p.Version.Version) } func (p *Product) PrimaryColumnValuesBySlug(slug string) map[string]string { segs := strings.Split(slug, \"_\") if len(segs) != 2 { panic(\"wrong slug\") } return map[string]string{ \"id\": segs[0], \"version\": segs[1], } } Create publisher and configure Publish view for model, and remember to display Status and Schedule fields in Editing mb := b.Model(\u0026Product{}) mb.Editing(\"StatusBar\", \"Schedule\", \"Name\", \"Price\") publisher := publish.New(DB, nil) publish_view.Configure(b, DB, nil, publisher, mb) // run the publisher job if Schedule is used go publish.RunPublisher(DB, nil, publisher) Implement the publish interfaces if there is a need to publish content to storage(filesystem, AWS S3, ...) var _ publish.PublishInterface = (*Product)(nil) var _ publish.UnPublishInterface = (*Product)(nil) func (p *Product) GetPublishActions(db *gorm.DB, ctx context.Context, storage oss.StorageInterface) (objs []*publish.PublishAction, err error) { // create publish actions return } func (p *Product) GetUnPublishActions(db *gorm.DB, ctx context.Context, storage oss.StorageInterface) (objs []*publish.PublishAction, err error) { // create unpublish actions return } Check the demo | Source on GitHub Modules Status Status module stores the status of the record. const ( StatusDraft = \"draft\" StatusOnline = \"online\" StatusOffline = \"offline\" ) type Status struct { Status string `gorm:\"default:'draft'\"` OnlineUrl string } The initial status is draft , after publishing it becomes online , and after unpublishing it becomes offline . Schedule Schedule module schedules records to be online/offline automatically with the publisher job. type Schedule struct { ScheduledStartAt *time.Time `gorm:\"index\"` ScheduledEndAt *time.Time `gorm:\"index\"` ActualStartAt *time.Time ActualEndAt *time.Time } If a record has ScheduledStartAt set, and the current time is larger than this value, the record will be published and the ActualStartAt will be set to the actual published time, the ScheduledStartAt will be cleared. If a record has ScheduledEndAt set, and the current time is larger than this value, the record will be unpublished and the ActualEndAt will be set to the actual unpublished time, the ScheduledEndAt will be cleared. Version Version module allows one record to have multiple copies, with Schedule, you can even schedule different prices of a product for a whole year. type Version struct { Version string `gorm:\"primary_key;size:128\"` VersionName string ParentVersion string } The Version will be the primary key. By default, the Version value will be YYYY-MM-DD-vSeq , e.g. 2006-01-02-v01 . And you can rename a version on interface, which will modify the value of VersionName . List List module publishes list page of resource. type List struct { PageNumber int Position int ListDeleted bool ListUpdated bool } "},{"URL":"basics/i18n.html","Title":"Internationalization","Body":"The i18n package provides support for internationalization (i18n) in Go applications.\nWith the package, you can support multiple languages,\nregister messages for each module in each language, and serve multilingual content\nbased on the user's preferences. Check the demo | Source on GitHub Getting Started To use the i18n package, you first need to import it into your Go application: import \"github.com/qor5/x/i18n\" Next, create a new Builder instance using the New() function.\nIf you want to use it with QOR5, use the I18n() on presets.Builder : i18nB := b.I18n() The Builder struct is the central point of the i18n package.\nIt holds the supported languages, the messages for each module in each language,\nand the configuration for retrieving the language preference. Adding Support Languages To support multiple languages in your web application, you need to define the languages that you support.\nYou can do this by calling the SupportLanguages function on the Builder struct: i18nB.SupportLanguages(language.English, language.SimplifiedChinese, language.Japanese) The i18n package uses English as the default language. You can add other languages by the SupportLanguages function. Registering Module Messages Once you have defined the languages, you need to register messages for each module.\nYou can do this by the RegisterForModule function on the Builder struct: i18nB. RegisterForModule(language.SimplifiedChinese, presets.ModelsI18nModuleKey, Messages_zh_CN_ModelsI18nModuleKey). RegisterForModule(language.Japanese, presets.ModelsI18nModuleKey, Messages_ja_JP_ModelsI18nModuleKey). RegisterForModule(language.English, I18nExampleKey, Messages_en_US). RegisterForModule(language.Japanese, I18nExampleKey, Messages_ja_JP). RegisterForModule(language.SimplifiedChinese, I18nExampleKey, Messages_zh_CN). GetSupportLanguagesFromRequestFunc(func(r *http.Request) []language.Tag { return b.I18n().GetSupportLanguages() }) The RegisterForModule function takes three arguments: the language tag, the module key,\nand a pointer to a struct that implements the Messages interface.\nThe Messages interface is an empty interface that you can use to define your own messages. Such a struct might look like this: const I18nExampleKey i18n.ModuleKey = \"I18nExampleKey\" type Messages struct { Admin string Welcome string } var Messages_en_US = \u0026Messages{ Admin: \"Admin\", Welcome: \"Welcome\", } var Messages_zh_CN = \u0026Messages{ Admin: \"管理系统\", Welcome: \"欢迎\", } var Messages_ja_JP = \u0026Messages{ Admin: \"管理システム\", Welcome: \"ようこそ\", } If you want to define messages inside the system,\nyou can add new variables to the message structure associated with presets.ModelsI18nModuleKey ,\nand the variable name definitions follow the camel case. Such a struct might look like this: type Messages_ModelsI18nModuleKey struct { Homes string Videos string VideosName string VideosDescription string } var Messages_zh_CN_ModelsI18nModuleKey = \u0026Messages_ModelsI18nModuleKey{ Homes: \"主页\", Videos: \"视频\", VideosName: \"视频名称\", VideosDescription: \"视频描述\", } var Messages_ja_JP_ModelsI18nModuleKey = \u0026Messages_ModelsI18nModuleKey{ Homes: \"ホーム\", Videos: \"ビデオ\", VideosName: \"ビデオの名前\", VideosDescription: \"ビデオの説明\", } The GetSupportLanguagesFromRequestFunc is a method of the Builder struct in the i18n package.\nIt allows you to set a function that retrieves the list of supported languages\nfrom an HTTP request, which can be useful in scenarios where the list of supported\nlanguages varies based on the request context. If you create a separate page, you need to use the EnsureLanguage to get i18n to work on this page. The EnsureLanguage function is an HTTP middleware that ensures the request's language\nis properly set and stored. It does this by first checking the query parameters for\na language value, and if found, setting a cookie with that value. If no language\nvalue is present in the query parameters, it looks for the language value in the cookie. The middleware then determines the best-matching language from the supported languages\nbased on the \"Accept-Language\" header of the request. If no match is found,\nit defaults to the first supported language. It then sets the language context for\nthe request, which can be retrieved later by calling the MustGetModuleMessages function. Retrieving Messages To retrieve module messages in your HTTP handler, you can use the MustGetModuleMessages function: msgr := i18n.MustGetModuleMessages(ctx.R, I18nExampleKey, Messages_en_US).(*Messages) r.Body = v.VContainer( h.Div( h.H1(msgr.Welcome), ).Class(\"text-center mt-8\"), ) The MustGetModuleMessages function takes three arguments:\nthe HTTP request, the module key, and a pointer to a struct\nthat implements the Messages interface. The function retrieves the messages\nfor the specified module in the language set by the i18n middleware. "},{"URL":"basics/l10n.html","Title":"Localization","Body":"L10n gives your models the ability to localize for different Locales. It can be a catalyst for the adaptation of a product, application, or document content to meet the language, cultural, and other requirements of a specific target market. Define a struct Define a struct that requires embed l10n.Locale . Also this struct must implement PrimarySlug() string and PrimaryColumnValuesBySlug(slug string) map[string]string . type L10nModel struct { gorm.Model Title string l10n.Locale } func (lm *L10nModel) PrimarySlug() string { return fmt.Sprintf(\"%v_%v\", lm.ID, lm.LocaleCode) } func (lm *L10nModel) PrimaryColumnValuesBySlug(slug string) map[string]string { segs := strings.Split(slug, \"_\") if len(segs) != 2 { panic(\"wrong slug\") } return map[string]string{ \"id\": segs[0], \"locale_code\": segs[1], } } Init a l10n builder Register locales here. You can use GetSupportLocaleCodesFromRequestFunc to determine who can use which locales. l10nBuilder := l10n.New() l10nBuilder. RegisterLocales(\"International\", \"international\", \"International\"). RegisterLocales(\"China\", \"cn\", \"China\"). RegisterLocales(\"Japan\", \"jp\", \"Japan\"). GetSupportLocaleCodesFromRequestFunc(func(R *http.Request) []string { return l10nBuilder.GetSupportLocaleCodes()[:] }) Configure the model builder Use l10n_view.Configure() func to configure l10n view. The Switch Locale ui will appear below the Brand . The Localize ui will appear in the RowMenuItem under the Edit and the Delete . Localize button is used to copy a piece of data from the current locale to the other locales. mb := b.Model(\u0026L10nModel{}).URIName(\"l10n-models\") l10n_view.Configure(b, DB, l10nBuilder, nil, mb) mb.Listing(\"ID\", \"Title\", \"Locale\") Full Example import ( \"fmt\" \"net/http\" \"strings\" \"github.com/qor5/admin/l10n\" l10n_view \"github.com/qor5/admin/l10n/views\" \"github.com/qor5/admin/presets\" \"github.com/qor5/admin/presets/gorm2op\" \"gorm.io/gorm\" ) type L10nModel struct { gorm.Model Title string l10n.Locale } func (lm *L10nModel) PrimarySlug() string { return fmt.Sprintf(\"%v_%v\", lm.ID, lm.LocaleCode) } func (lm *L10nModel) PrimaryColumnValuesBySlug(slug string) map[string]string { segs := strings.Split(slug, \"_\") if len(segs) != 2 { panic(\"wrong slug\") } return map[string]string{ \"id\": segs[0], \"locale_code\": segs[1], } } func LocalizationExampleMock(b *presets.Builder) { if err := DB.AutoMigrate(\u0026L10nModel{}); err != nil { panic(err) } b.URIPrefix(LocalizationExamplePath). DataOperator(gorm2op.DataOperator(DB)) l10nBuilder := l10n.New() l10nBuilder. RegisterLocales(\"International\", \"international\", \"International\"). RegisterLocales(\"China\", \"cn\", \"China\"). RegisterLocales(\"Japan\", \"jp\", \"Japan\"). GetSupportLocaleCodesFromRequestFunc(func(R *http.Request) []string { return l10nBuilder.GetSupportLocaleCodes()[:] }) mb := b.Model(\u0026L10nModel{}).URIName(\"l10n-models\") l10n_view.Configure(b, DB, l10nBuilder, nil, mb) mb.Listing(\"ID\", \"Title\", \"Locale\") Check the demo | Source on GitHub "},{"URL":"basics/page-func-and-event-func.html","Title":"Page Func and Event Func","Body":"PageFunc is used to build a web page, EventFunc is called when user interact with the page, For example button or link clicks. type PageFunc func(ctx *EventContext) (r PageResponse, err error) type EventFunc func(ctx *EventContext) (r EventResponse, err error) web.Page(...) converts multiple EventFunc s along with one PageFunc to a http.Handler ,\nevent 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 Here is a hello world with more interactions. User click the button will reload the page with latest time 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\" Check the demo | Source on GitHub 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 mux.Handle( e00_basics.HelloWorldReloadPath, e00_basics.HelloWorldReloadPB.Wrap(demoLayout), ) wb.Page(...) convert any PageFunc into http.Handler , outside you can wrap any middleware that can use on Go standard http.Handler . In case you don't know what is a http.Handler middleware,\nIt's a function that takes http.Handler as input, might also with other parameters,\nAnd also return a new http.Handler, gziphandler is an example. But What the heck is demoLayout there?\nWell it's a PageFunc middleware. That takes an PageFunc as input,\nwrap it's PageResponse with layout html and return a new PageFunc .\nIf you follow the code to write your own PageFunc ,\nThe button click might not work without this.\nSince there is no layout to import needed javascript to make this work.\ncontinue to next page to checkout how to add necessary javascript, css etc to make the demo work. "},{"URL":"advanced-functions/the-go-html-builder.html","Title":"The Go HTML builder","Body":"Like at the beginning we said, That we don't use interpreted template language (eg go html/template)\nto 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 is just for that. Although Go can't do flexible builder syntax like Kotlin does,\nBut it can also do quite well. Consider the following code: 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\" It's basically assembled what Kotlin can do, Also is legitimate Go code. Check the demo | Source on GitHub "},{"URL":"presets-guide/its-the-whole-house.html","Title":"Not just scaffolding, it's the whole house","Body":"Presets let you config generalized data management UI interface for database.\nIt's not a scaffolding to generate source code. But provide more abstract and\nflexible API to enrich features along the way. package e21_presents import ( \"fmt\" \"net/url\" \"os\" \"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/postgres\" \"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(postgres.Open(os.Getenv(\"DB_PARAMS\")), \u0026gorm.Config{}) if err != nil { panic(err) } db.Logger.LogMode(logger.Info) err = db.AutoMigrate( \u0026Customer{}, \u0026Company{}, \u0026Address{}, ) 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(\u0026Customer{}) return } const PresetsHelloWorldPath = \"/samples/presets-hello-world\" And this *presets.Builder instance is actually also a http.Handler , So that we can mount it\nto the http serve mux directly like this: c00 := presets.New().AssetFunc(addGA) e21_presents.PresetsHelloWorld(c00) mux.Handle( e21_presents.PresetsHelloWorldPath+\"/\", c00, ) Check the demo | Source on GitHub With r.Model(\u0026Customer{}) : 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 "},{"URL":"vuetify-components/lazy-portals.html","Title":"Lazy Portals","Body":"Use web.Portal().Loader(web.POST().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. 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 = \u0026mystate{} ctx.MustUnmarshalForm(s) textField := VTextField().FieldName(\"Company\") if len(s.Error) \u003e 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 = \u0026mystate{} ctx.MustUnmarshalForm(s) if len(s.Company) \u003c 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, \u0026web.PortalUpdate{ Name: \"portalC\", Body: h.Text(fmt.Sprint(time.Now().UnixNano())), }, \u0026web.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\" Check the demo | Source on GitHub "},{"URL":"basics/layout-function-and-page-injector.html","Title":"Layout Function and Page Injector","Body":"Read this code first, Guess what it does. func demoLayout(in web.PageFunc) (out web.PageFunc) { return func(ctx *web.EventContext) (pr web.PageResponse, err error) { addGA(ctx) ctx.Injector.HeadHTML(` \u003cscript src='/assets/vue.js'\u003e\u003c/script\u003e `) ctx.Injector.TailHTML(coreJSTags) ctx.Injector.HeadHTML(` \u003cstyle\u003e [v-cloak] { display: none; } \u003c/style\u003e `) var innerPr web.PageResponse innerPr, err = in(ctx) if err != nil { panic(err) } pr.Body = innerPr.Body return } } ctx.Injector is for inject html into default layout's html head, and bottom of body.\nhtml head normally for page title, keywords etc all kinds meta data, and css styles,\njavascript 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: mux.Handle(\"/assets/main.js\", web.PacksHandler(\"text/javascript\", web.JSComponentsPack(), ), ) mux.Handle(\"/assets/vue.js\", web.PacksHandler(\"text/javascript\", web.JSVueComponentsPack(), ), ) web.JSComponentsPack is the production version of QOR5 core javascript code.\nCreated by using @vue/cli ,\nIt does the basic functions like render server side returned html as vue templates.\nProvide basic event functions that call to server, and manage push state\n(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 . import ( \"embed\" ) //go:embed corejs/dist/*.js var box embed.FS func JSComponentsPack() ComponentsPack { v, err := box.ReadFile(\"corejs/dist/core.umd.min.js\") if err != nil { panic(err) } return ComponentsPack(v) } func JSVueComponentsPack() ComponentsPack { v, err := box.ReadFile(\"corejs/dist/vue.min.js\") if err != nil { panic(err) } return ComponentsPack(v) } And with web.PacksHandler , You can merge multiple javascript or css assets together into one url.\nSo that browser only need to request them one time. and cache them. The cache is set to the start\ntime of the process. So next time the app restarts, it invalid the cache. Summary For a new project: Use @vue/cli to create an asset project that manage your javascript and css. and compile them for production use Use 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 "},{"URL":"basics/switch-pages-with-push-state.html","Title":"Switch Pages with Push State","Body":"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 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: 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) \u003e 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) Check the demo | Source on GitHub When running the above demo, If you check Chrome Developer Tools about Network requests,\nYou 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 ,\nBut it is default added to every web.PageFunc . So that the web page can\nboth respond to normal HTTP request from Browser, Search Engine, Or from\nother pages in the same web app that can do push state link. Summary 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: Go to a new page Reload the whole current page 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. "},{"URL":"basics/reload-page-with-a-flash.html","Title":"Reload Page with a Flash","Body":"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: 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 = \u0026Data1{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\" Check the demo | Source on GitHub 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 .\nDifferent 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. "},{"URL":"basics/partial-refresh-with-portal.html","Title":"Partial Refresh with Portal","Body":"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: 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, \u0026web.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\" Check the demo | Source on GitHub web.Portal().Name(\"part1\") Place a placeholder inside you page, and append web.PortalUpdate to er.UpdatePortals to update the portal with that name.\nMultiple portal can be updated at the same time. Load Portal in separate AJAX request With web.Portal , We can also load the portal with a separate AJAX request after page load.\nIt is useful for the type of the content is not that important to the page, But load them are\nquite heavy. Like related products of a product detail page of a ECommerce site. 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(` \u003cstyle\u003e .rp { float: left; width: 200px; height: 200px; margin-right: 20px; background-color: orange; } \u003c/style\u003e `, ) 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 \u003e 5 { er.VarsScript = `vars.interval = 0;` } return } func loadData(ctx *web.EventContext) (er web.EventResponse, err error) { var r []string for i := 0; i \u003c 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\" Check the demo | Source on GitHub 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 \u0026 Async Components , to load Go generated html (vue runtime templates)\nfrom 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. "},{"URL":"basics/manipulate-page-url-in-event-func.html","Title":"Manipulate Page URL in Event Func","Body":"Encode page state into query strings in url is useful. because user can paste the link to another person,\nThat can open the page to the exact state of the page being sent, Not the initial state of the page. For example: 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\")) \u003e 0 { title = ctx.R.URL.Query().Get(\"title\") } var panel HTMLComponent if len(ctx.R.URL.Query().Get(\"panel\")) \u003e 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\" Check the demo | Source on GitHub 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: A().Text(\"change page title\").Href(\"javascript:;\").\n\tAttr(\"@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 "},{"URL":"basics/summary-of-event-response.html","Title":"Summary of Event Response","Body":"The behaviour of web.EventFunc is controlled by it's return type web.EventResponse type EventResponse struct { PageTitle string `json:\"pageTitle,omitempty\"` Body h.HTMLComponent `json:\"body,omitempty\"` Reload bool `json:\"reload,omitempty\"` PushState *LocationBuilder `json:\"pushState\"` // This we don't omitempty, So that {} can be kept when use url.Values{} RedirectURL string `json:\"redirectURL,omitempty\"` // change window url without push state ReloadPortals []string `json:\"reloadPortals,omitempty\"` UpdatePortals []*PortalUpdate `json:\"updatePortals,omitempty\"` Data interface{} `json:\"data,omitempty\"` // used for return collection data like TagsInput data source VarsScript string `json:\"varsScript,omitempty\"` // used with InitContextVars to set values for example vars.show to used by v-model } PageTitle set the html head title, It not only set when render html page directly which is\nrequest the url directly from the browser. Also use javascript to set the page title when you do\npush 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\nhtml 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\nevent 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\nhead 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\nwill marshals to JSON, and on the client side use javascript to utilize them "},{"URL":"basics/scope-component.html","Title":"Scope Component","Body":"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') .\nWe 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: 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) Check the demo | Source on GitHub 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 ) 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 .\nIn this way, you can submit Raw Material ID directly in the Material Form . For example: 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) Check the demo | Source on GitHub Use web.Scope().VSlot(\"{ plaidForm }\") to determine the scope of a form. "},{"URL":"basics/event-handling.html","Title":"Event Handling","Body":"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 .\nThe 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 URL Request a page. 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 } Check the demo | Source on GitHub PushState Reqest a page and also changing the window location. 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 } Check the demo | Source on GitHub Reload Refresh page. 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 } Check the demo | Source on GitHub Query Request a page with a query. 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 } Check the demo | Source on GitHub MergeQuery Request a page with merging a query. 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\u0026name=qor5\u0026email=qor5@theplant.jp\").PushState(true).Query(\"address\", \"tokyo\").MergeQuery(true).Go())), ), ) return } Check the demo | Source on GitHub ClearMergeQuery Request a page with clearing a query. 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\u0026name=qor5\u0026email=qor5@theplant.jp\").PushState(true).Query(\"address\", \"tokyo\").ClearMergeQuery([]string{\"name\"}).Go())), ), ) return } Check the demo | Source on GitHub StringQuery Request a page with a query string. 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 } Check the demo | Source on GitHub Queries Request a page with url.Values. 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 } Check the demo | Source on GitHub PushStateURL Request a page with a url and also changing the window location. 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 } Check the demo | Source on GitHub Location Open a page with more options. 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(\u0026web.LocationBuilder{MyURL: EventExamplePagePath, MyStringQuery: \"address=test\"}).Go())), ), ) return } Check the demo | Source on GitHub FieldValue Fill in a value on form. 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 } Check the demo | Source on GitHub FormClear Clear all form data. 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 } Check the demo | Source on GitHub EventFunc Register an event func and call it when the event is triggered. func EventHandlingEventFunc(ctx *web.EventContext) (pr web.PageResponse, err error) { pr.Body = Div( VBtn(\"Go\").Attr(\"@click\", web.POST().EventFunc(\"hello\").Go()), ) return } Check the demo | Source on GitHub Script Run a script code. 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 } Check the demo | Source on GitHub Raw Directly call the js method 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 } Check the demo | Source on GitHub "},{"URL":"basics/form-handling.html","Title":"Form Handling","Body":"Form handling is an important part of web development. to make handling form easy,\nwe have a global form that always be submitted with any event func. What you need to do\nis just to give an input a name. For example: import ( \"fmt\" \"io\" \"mime/multipart\" \"github.com/qor5/docs/docsrc/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(\u0026fv) 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\" Check the demo | Source on GitHub Use .Attr(web.VFieldName(\"Abc\")...) to set the field name, make the name matches your data struct field name.\nSo that you can ctx.UnmarshalForm(\u0026fv) 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. "},{"URL":"vuetify-components/basic-inputs.html","Title":"Basic Inputs","Body":"Vuetify provides many form basic inputs, and also with error messages display on fields. Here is one example: import ( \"mime/multipart\" \"github.com/qor5/docs/docsrc/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 = \u0026myFormValue{ 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, \u0026web.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 = \u0026myFormValue{} ctx.MustUnmarshalForm(s) verr := web.ValidationErrors{} if len(s.MyValue) \u003c 10 { verr.FieldError(\"MyValue\", \"my value is too small\") } if len(s.TextareaValue) \u003e 5 { verr.FieldError(\"TextareaValue\", \"textarea value is too large\") } if !s.Agreed { verr.FieldError(\"Agreed\", \"You must agree the terms\") } if s.Slider1 \u003e 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) Check the demo | Source on GitHub "},{"URL":"vuetify-components/a-taste-of-using-vuetify-in-go.html","Title":"A Taste of using Vuetify in Go","Body":"Vuetify is a really mature Vue components library for Material Design . We have made the efforts to\nintegrate most all of it as a go package. You can use it with ease just like any\nother go package. Use container, toolbar, list, list item etc This example is purely render, we didn't integrate any interaction (event func) to it. 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.vuetifyjs.com/images/lists/1.jpg\"), ), VListItemContent( VListItemTitle(h.Text(\"Brunch this weekend?\")), VListItemSubtitle( h.Span(\"Ali Connors\").Class(\"text--primary\"), h.Text(\"\u0026mdash; 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.vuetifyjs.com/images/lists/2.jpg\"), ), VListItemContent( VListItemTitle(h.RawHTML(`Summer BBQ \u003cspan class=\"grey--text text--lighten-1\"\u003e4\u003c/span\u003e`)), VListItemSubtitle(h.RawHTML(`\u003cspan class='text--primary'\u003eto Alex, Scott, Jennifer\u003c/span\u003e \u0026mdash; Wish I could come, but I'm out of town this weekend.`)), ), ), VDivider().Inset(true), VListItem( VListItemAvatar( h.Img(\"https://cdn.vuetifyjs.com/images/lists/3.jpg\"), ), VListItemContent( VListItemTitle(h.Text(`Oui oui`)), VListItemSubtitle(h.RawHTML(`\u003cspan class='text--primary'\u003eSandra Adams\u003c/span\u003e \u0026mdash; Do you have Paris recommendations? Have you ever been?`)), ), ), ).TwoLine(true), ) return } var HelloVuetifyListPB = web.Page(HelloVuetifyList) const HelloVuetifyListPath = \"/samples/hello-vuetify-list\" Check the demo | Source on GitHub Use menu, card, list, etc This example uses the menu popup, card, list component. and some interactions of clicking\nbuttons on the menu popup. import ( \"github.com/qor5/docs/docsrc/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(\u0026fv) 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.vuetifyjs.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, \u0026web.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\" .Attr(web.InitContextVars, \"{myMenuShow: false}\") is a special vue directive that\nwe 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\nthe initialization. It's often useful to control dialog, popups. At this example,\nWe add it, So that the cancel button on the menu, could actually close the menu without\nrequesting server backend. toggleFavored event func did an partial update only to the favorite icon button. So that it won't close the\nmenu popup, but updated the button to toggle the favorite icon. Check the demo | Source on GitHub "},{"URL":"vuetify-components/linkage-select.html","Title":"Linkage Select","Body":"LinkageSelect is a component for multi-level linkage select. 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\" Check the demo | Source on GitHub Filter intergation 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(\u0026Address{}) 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\u0026City\u0026District\", 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: \"吴江区\"}, }, } } Check the demo | Source on GitHub "},{"URL":"vuetify-components/auto-complete.html","Title":"Auto Complete","Body":"AutoComplete is a more advanced component that vuetify provides, We extend it\nSo 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 import ( \"fmt\" \"os\" \"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/postgres\" \"gorm.io/gorm\" ) type ( User struct { Login string Name string } UserIcons struct { Login string `json:\"text\"` Name string `json:\"value\"` Icon string `json:\"icon\"` } Product struct { ID uint `gorm:\"primarykey\"` Name string } ) var ( options = []*User{ {Login: \"sam\", Name: \"Sam\"}, {Login: \"john\", Name: \"John\"}, {Login: \"charles\", Name: \"Charles\"}, } iconOptions = []*UserIcons{ {Login: \"sam\", Name: \"Sam\", Icon: \"https://cdn.vuetifyjs.com/images/lists/1.jpg\"}, {Login: \"john\", Name: \"John\", Icon: \"https://cdn.vuetifyjs.com/images/lists/2.jpg\"}, {Login: \"charles\", Name: \"Charles\", Icon: \"https://cdn.vuetifyjs.com/images/lists/3.jpg\"}, } loadMoreRes *vuetifyx.AutocompleteDataSource pagingRes *vuetifyx.AutocompleteDataSource ExamplePreset *presets.Builder ) func init() { db, err := gorm.Open(postgres.Open(os.Getenv(\"DB_PARAMS\")), \u0026gorm.Config{}) if err != nil { panic(err) } db.AutoMigrate(\u0026Product{}) db.Where(\"1=1\").Delete(\u0026Product{}) for i := 1; i \u003c 300; i++ { db.Create(\u0026Product{Name: fmt.Sprintf(\"Product %d\", i)}) } ExamplePreset = presets.New() ExamplePreset.URIPrefix(VuetifyAutoCompletePresetPath).DataOperator(gorm2op.DataOperator(db)) listing := ExamplePreset.Model(\u0026Product{}).Listing() loadMoreRes = listing.ConfigureAutocompleteDataSource( \u0026presets.AutocompleteDataSourceConfig{ OptionValue: \"ID\", OptionText: \"Name\", OptionIcon: func(product interface{}) string { return fmt.Sprintf(\"https://cdn.vuetifyjs.com/images/lists/%d.jpg\", product.(*Product).ID%4+1) }, KeywordColumns: []string{ \"Name\", }, PerPage: 50, }, \"loadMore\", ) pagingRes = listing.ConfigureAutocompleteDataSource( \u0026presets.AutocompleteDataSourceConfig{ OptionValue: \"ID\", OptionText: \"Name\", OptionIcon: func(product interface{}) string { return fmt.Sprintf(\"https://cdn.vuetifyjs.com/images/lists/%d.jpg\", product.(*Product).ID%4+1) }, KeywordColumns: []string{ \"Name\", }, PerPage: 20, IsPaging: true, OrderBy: \"Name\", }, \"paging\", ) } func VuetifyAutocomplete(ctx *web.EventContext) (pr web.PageResponse, err error) { pr.Body = VContainer( h.H1(\"Select many (default)\"), vuetifyx.VXAutocomplete(). Label(\"Load options from a list\"). Items(options). FieldName(\"Values1\"). ItemText(\"Name\"). ItemValue(\"Login\"), h.H1(\"Select one\"), vuetifyx.VXAutocomplete(). FieldName(\"Values2\"). Label(\"Load options from a list\"). Items(options). ItemText(\"Name\"). ItemValue(\"Login\"). Multiple(false), h.H1(\"Has icon\"), vuetifyx.VXAutocomplete(). Label(\"Load options from a list\"). Items(iconOptions). HasIcon(true), h.H1(\"Load more from remote resource\"), vuetifyx.VXAutocomplete(). FieldName(\"Values2\"). Label(\"Load options from data source\"). SetDataSource(loadMoreRes), h.H1(\"Paging with remote resource\"), vuetifyx.VXAutocomplete(). FieldName(\"Values2\"). Label(\"Load options from data source\"). SetDataSource(pagingRes), h.H1(\"Sorting\"), vuetifyx.VXAutocomplete(). FieldName(\"Values2\"). Label(\"Load options from data source\"). Sorting(true). SetDataSource(pagingRes).ChipColor(\"red\"), ) return } var VuetifyAutocompletePB = web.Page(VuetifyAutocomplete) const VuetifyAutoCompletePath = \"/samples/vuetify-auto-complete\" const VuetifyAutoCompletePresetPath = \"/samples/vuetify-auto-complete-preset\" Check the demo | Source on GitHub "},{"URL":"components-guide/composite-new-component-with-go.html","Title":"Composite new Component With Go","Body":"Any Go function that returns an htmlgo.HTMLComponent is a component,\nAny Go struct that implements MarshalHTML(ctx context.Context) ([]byte, error) function is an component.\nThey can be composite into a new component very easy. This example is ported from Bootstrap4 Navbar : 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\" Check the demo | Source on GitHub You can see from the example, We have created Navbar and Carousel components by\nsimply create Go func that returns htmlgo.HTMLComponent .\nIt is easy to pass in components as parameter, and wrap components.\nBy 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: func demoBootstrapLayout(in web.PageFunc) (out web.PageFunc) { return func(ctx *web.EventContext) (pr web.PageResponse, err error) { addGA(ctx) ctx.Injector.HeadHTML(` \u003clink rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css\" integrity=\"sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T\" crossorigin=\"anonymous\"\u003e \u003cscript src='/assets/vue.js'\u003e\u003c/script\u003e `) ctx.Injector.TailHTML(` \u003cscript src=\"https://code.jquery.com/jquery-3.3.1.slim.min.js\" integrity=\"sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo\" crossorigin=\"anonymous\"\u003e\u003c/script\u003e \u003cscript src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js\" integrity=\"sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1\" crossorigin=\"anonymous\"\u003e\u003c/script\u003e \u003cscript src=\"https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js\" integrity=\"sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM\" crossorigin=\"anonymous\"\u003e\u003c/script\u003e \u003cscript src='/assets/main.js'\u003e\u003c/script\u003e `) ctx.Injector.HeadHTML(` \u003cstyle\u003e [v-cloak] { display: none; } \u003c/style\u003e `) var innerPr web.PageResponse innerPr, err = in(ctx) if err != nil { panic(err) } pr.Body = innerPr.Body return } } You can utilize the command line tool html2go to convert existing html code to htmlgo code.\nBy 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! "},{"URL":"components-guide/integrate-a-heavy-vue-component.html","Title":"Integrate a heavy Vue Component","Body":"We can abstract any complicated of server side render component with htmlgo .\nBut a lots of components in the modern web have done many things on the client side. means there are many logic\nhappens 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\nneed to communicate with server. We are going to integrate the fantastic rich text editor tiptap to be used as any htmlgo.HTMLComponent . Step 1 : Create a @vue/cli project : $ vue create tiptapjs Modify or add a separate vue.config.js config file, const {defineConfig} = require('@vue/cli-service'); module.exports = defineConfig({ transpileDependencies: true, runtimeCompiler: true, productionSourceMap: false, devServer: { port: 3500, }, configureWebpack: { output: { libraryExport: 'default', }, externals: {vue: 'Vue'}, }, chainWebpack: config =\u003e { const svgRule = config.module.rule('svg').clear(); svgRule. test(/\\.(svg)(\\?.*)?$/). use('babel-loader'). loader('babel-loader'). end(). use('vue-svg-loader'). loader('vue-svg-loader'); }, }); 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,\nSince 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. export default { components: { EditorContent, EditorMenuBar, Icon, }, props: { value: String, }, data() { return { editor: new Editor({ content: this.$props.value, extensions: extensions(), onUpdate: ({getHTML}) =\u003e { const html = getHTML(); this.$emit(\"input\", html) }, }) } }, beforeDestroy() { this.editor.destroy() } } We injected the this.$plaid() . that is from web/corejs , Which you will need to use\nFor every Go Plaid web applications. Here we uses one function fieldValue from it.\nIt 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.\nAlso allow component user to set fieldName , which is important when posting the value to the\nserver. Step 3 : At main.js , Use a special hook to register the component to web/corejs import TipTapEditor from './editor.vue' (window.__goplaidVueComponentRegisters = window.__goplaidVueComponentRegisters || []).push((Vue) =\u003e { Vue.component('tiptap-editor', TipTapEditor) }); Step 4 : Test the component in a simple html We edited the index.html inside public to be the following: \u003chead\u003e \u003cmeta charset=\"utf-8\"\u003e \u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\"\u003e \u003cscript src=\"https://cdn.jsdelivr.net/npm/vue/dist/vue.js\"\u003e\u003c/script\u003e \u003ctitle\u003etiptapjs\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e \u003cdiv id=\"app\"\u003e \u003ctiptap-editor field-name=\"content1\" value='\u0026lt;h1\u0026gt;header\u0026lt;/h1\u0026gt;\u0026lt;p\u0026gt;abc\u0026lt;/p\u0026gt;'\u003e\u003c/tiptap-editor\u003e \u003c/div\u003e \u003cscript src=\"http://localhost:3500/app.js\"\u003e\u003c/script\u003e \u003cscript src=\"http://localhost:3100/app.js\"\u003e\u003c/script\u003e \u003c/body\u003e For http://localhost:3500/app.js to be able to serve. you have to run yarn serve in\ntiptapjs directory. http://localhost:3100/app.js is QOR5 web corejs vue project.\nSo 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\nBrowser to the index.html file, and see if your vue component can render and behave correctly. Step 5 : Use packr to pack the dist folder We write a packr box inside tiptapjs.go along side the tiptapjs folder. import ( \"embed\" \"github.com/qor5/web\" ) //go:embed tiptapjs/dist var box embed.FS func JSComponentsPack() web.ComponentsPack { v, err := box.ReadFile(\"tiptapjs/dist/tiptap.umd.min.js\") if err != nil { panic(err) } return web.ComponentsPack(v) } func CSSComponentsPack() web.ComponentsPack { v, err := box.ReadFile(\"tiptapjs/dist/tiptap.css\") if err != nil { panic(err) } return web.ComponentsPack(v) } And write a build.sh to build the javascript to production version, and run packr to pack\nthem into a_tiptap-packr.go file. CUR=$(pwd)/$(dirname $0) if test \"$1\" = 'clean'; then echo \"Removing node_modules\" rm -rf $CUR/tiptapjs/node_modules/ fi rm -r $CUR/tiptapjs/dist echo \"Building tiptapjs\" cd $CUR/tiptapjs \u0026\u0026 npm install \u0026\u0026 npm run build Step 6 : Write a Go wrapper to wrap it to be a HTMLComponent import ( \"context\" \"github.com/qor5/web\" h \"github.com/theplant/htmlgo\" ) type TipTapEditorBuilder struct { tag *h.HTMLTagBuilder } func TipTapEditor() (r *TipTapEditorBuilder) { r = \u0026TipTapEditorBuilder{ tag: h.Tag(\"tiptap-editor\"), } return } func (b *TipTapEditorBuilder) FieldName(v string) (r *TipTapEditorBuilder) { b.tag.Attr(web.VFieldName(v)...) return b } func (b *TipTapEditorBuilder) Value(v string) (r *TipTapEditorBuilder) { b.tag.Attr(\":value\", h.JSONString(v)) return b } func (b *TipTapEditorBuilder) MarshalHTML(ctx context.Context) (r []byte, err error) { return b.tag.MarshalHTML(ctx) } Step 7 : Use it in your web app To use it, first we have to mount the assets into our app mux.Handle(\"/assets/tiptap.js\", web.PacksHandler(\"text/javascript\", tiptap.JSComponentsPack(), ), ) mux.Handle(\"/assets/tiptap.css\", web.PacksHandler(\"text/css\", tiptap.CSSComponentsPack(), ), ) And reference them in our layout function. func tiptapLayout(in web.PageFunc) (out web.PageFunc) { return func(ctx *web.EventContext) (pr web.PageResponse, err error) { addGA(ctx) ctx.Injector.HeadHTML(` \u003clink rel=\"stylesheet\" href=\"/assets/tiptap.css\"\u003e \u003cscript src='/assets/vue.js'\u003e\u003c/script\u003e `) ctx.Injector.TailHTML(` \u003cscript src='/assets/tiptap.js'\u003e\u003c/script\u003e \u003cscript src='/assets/main.js'\u003e\u003c/script\u003e `) ctx.Injector.HeadHTML(` \u003cstyle\u003e [v-cloak] { display: none; } \u003c/style\u003e `) var innerPr web.PageResponse innerPr, err = in(ctx) if err != nil { panic(err) } pr.Body = innerPr.Body return } } And we write a page func to use it like any other component: 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 = ` \u003ch1\u003eHello\u003c/h1\u003e \u003cp\u003e This is a nice editor \u003c/p\u003e \u003cul\u003e \u003cli\u003e \u003cp\u003e 123 \u003c/p\u003e \u003c/li\u003e \u003cli\u003e \u003cp\u003e 456 \u003c/p\u003e \u003c/li\u003e \u003cli\u003e \u003cp\u003e 789 \u003c/p\u003e \u003c/li\u003e \u003c/ul\u003e ` } 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\" And now let's check out our fruits: Check the demo | Source on GitHub "},{"URL":"appendix/all-demo-examples.html","Title":"All Demo Examples","Body":"Vuetify List | Source Vuetify Menu | Source Presets Detail Page Top Notes | Source Presets Detail Page Details | Source Presets Detail Page Credit Cards | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Event Handling | Source Presets Hello World | Source Lazy Portals | Source Manipulate Page URL In Event Func | Source Vuetify Navigation Drawer | Source Page Func and Event Func | Source Partial Update | Source Partial Reload | Source Reload Page With a Flash | Source Switch Pages With Push State | Source The Go HTML Builder | Source Web Scope Use Locals | Source Web Scope Use PlaidForm | Source Vuetify AutoComplete | Source Vuetify Basic Inputs | Source Brand Title | Source Brand Func | Source Profile | Source Confirm Dialog | Source Presets Editing Customization Description Field | Source Presets Editing Customization File Type | Source Presets Editing Customization Tabs | Source Presets Editing Customization Validation | Source Basic filter | Source Form Handling | Source I18n | Source Vuetify LinkageSelect | Source LinkageSelect Filter Item | Source Presets Listing Customization Fields | Source Presets Listing Filters | Source Presets Listing Filter Tabs | Source Presets Listing Bulk Actions | Source Search Func | Source Presets Listing Customization Fields | Source L10n | Source Presets Detail Page Credit Cards | Source Menu Order | Source Menu Group | Source Notification Center | Source Publish | Source Shortcut | Source Vuetify Variant Sub Form | Source Worker | Source Action Worker | Source Composite New Component With Go | Source Integrate a Heavy Vue Component | Source Hello World | Source "}] diff --git a/docs/vuetify-components/lazy-portals.html b/docs/vuetify-components/lazy-portals.html index 981ab32..4d8d3eb 100644 --- a/docs/vuetify-components/lazy-portals.html +++ b/docs/vuetify-components/lazy-portals.html @@ -225,7 +225,7 @@
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.
Use web.Portal().Loader(web.POST().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.