~repos /gromer

#golang#htmx#ssr

git clone https://pyrossh.dev/repos/gromer.git

gromer is a framework and cli to build multipage web apps in golang using htmx and alpinejs.


2b7f14a5 Peter John

3 years ago
fix cmd
_example/components/todo.go CHANGED
@@ -5,11 +5,6 @@ import (
5
5
  . "github.com/pyros2097/gromer/gsx"
6
6
  )
7
7
 
8
- var _ = Css(`
9
- `)
10
-
11
- // <Checkbox hx-post="/" value="{todo.Completed}" />
12
-
13
8
  func Todo(c Context, todo *todos.Todo) *Node {
14
9
  return c.Render(`
15
10
  <li id="todo-{todo.ID}" class="{ completed: todo.Completed }">
_example/main.go CHANGED
@@ -4,6 +4,7 @@ package main
4
4
  import (
5
5
  "github.com/gorilla/mux"
6
6
  "github.com/pyros2097/gromer"
7
+ "github.com/pyros2097/gromer/gsx"
7
8
  "github.com/rs/zerolog/log"
8
9
  "gocloud.dev/server"
9
10
 
@@ -13,7 +14,6 @@ import (
13
14
  "github.com/pyros2097/gromer/_example/routes/404"
14
15
  "github.com/pyros2097/gromer/_example/routes"
15
16
  "github.com/pyros2097/gromer/_example/routes/about"
16
- "github.com/pyros2097/gromer/gsx"
17
17
 
18
18
  )
19
19
 
@@ -23,6 +23,7 @@ func init() {
23
23
 
24
24
  gsx.RegisterComponent(containers.TodoCount, "filter")
25
25
  gsx.RegisterComponent(containers.TodoList, "page", "filter")
26
+
26
27
  gromer.RegisterAssets(assets.FS)
27
28
  }
28
29
 
@@ -39,7 +40,6 @@ func main() {
39
40
  gromer.StylesRoute(staticRouter, "/styles.css")
40
41
 
41
42
  pageRouter := baseRouter.NewRoute().Subrouter()
42
- // gromer.ApiExplorerRoute(pageRouter, "/explorer")
43
43
  gromer.Handle(pageRouter, "GET", "/", routes.GET)
44
44
  gromer.Handle(pageRouter, "POST", "/", routes.POST)
45
45
  gromer.Handle(pageRouter, "GET", "/about", about.GET)
_example/routes/get.go CHANGED
@@ -5,6 +5,88 @@ import (
5
5
  . "github.com/pyros2097/gromer/gsx"
6
6
  )
7
7
 
8
+ var _ = Css(`
9
+ .container {
10
+ background: #fff;
11
+ margin: 130px 0 40px 0;
12
+ position: relative;
13
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
14
+ }
15
+
16
+ .container h1 {
17
+ position: absolute;
18
+ top: -155px;
19
+ width: 100%;
20
+ font-size: 100px;
21
+ font-weight: 100;
22
+ text-align: center;
23
+ color: rgba(175, 47, 47, 0.15);
24
+ -webkit-text-rendering: optimizeLegibility;
25
+ -moz-text-rendering: optimizeLegibility;
26
+ text-rendering: optimizeLegibility;
27
+ }
28
+
29
+ input::-webkit-input-placeholder {
30
+ font-style: italic;
31
+ font-weight: 300;
32
+ color: #e6e6e6;
33
+ }
34
+
35
+ input::-moz-placeholder {
36
+ font-style: italic;
37
+ font-weight: 300;
38
+ color: #e6e6e6;
39
+ }
40
+
41
+ input::input-placeholder {
42
+ font-style: italic;
43
+ font-weight: 300;
44
+ color: #e6e6e6;
45
+ }
46
+
47
+ .clear-completed, .clear-completed:active {
48
+ float: right;
49
+ position: relative;
50
+ line-height: 20px;
51
+ text-decoration: none;
52
+ cursor: pointer;
53
+ }
54
+
55
+ .clear-completed:hover {
56
+ text-decoration: underline;
57
+ }
58
+
59
+ .filters {
60
+ margin: 0;
61
+ padding: 0;
62
+ list-style: none;
63
+ position: absolute;
64
+ right: 0;
65
+ left: 0;
66
+ }
67
+
68
+ .filters li {
69
+ display: inline;
70
+ }
71
+
72
+ .filters li a {
73
+ color: inherit;
74
+ margin: 3px;
75
+ padding: 3px 7px;
76
+ text-decoration: none;
77
+ border: 1px solid transparent;
78
+ border-radius: 3px;
79
+ }
80
+
81
+ .filters li a:hover {
82
+ border-color: rgba(175, 47, 47, 0.1);
83
+ }
84
+
85
+ .filters li a.selected {
86
+ border-color: rgba(175, 47, 47, 0.2);
87
+ }
88
+ `)
89
+
8
90
  type GetParams struct {
9
91
  Page int `json:"page"`
10
92
  Filter string `json:"filter"`
@@ -16,7 +98,7 @@ func GET(c Context, params GetParams) (*Node, int, error) {
16
98
  c.Meta("author", "gromer")
17
99
  c.Meta("keywords", "gromer")
18
100
  return c.Render(`
19
- <div class="todoapp">
101
+ <div class="container">
20
102
  <header class="header">
21
103
  <h1>todos</h1>
22
104
  <form hx-post="/" hx-target="#todo-list" hx-swap="afterbegin" _="on htmx:afterOnLoad set #text.value to ''">
_example/routes/styles.go CHANGED
@@ -135,71 +135,6 @@ var _ = Css(`
135
135
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2);
136
136
  }
137
137
 
138
- .filters {
139
- margin: 0;
140
- padding: 0;
141
- list-style: none;
142
- position: absolute;
143
- right: 0;
144
- left: 0;
145
- }
146
-
147
- .filters li {
148
- display: inline;
149
- }
150
-
151
- .filters li a {
152
- color: inherit;
153
- margin: 3px;
154
- padding: 3px 7px;
155
- text-decoration: none;
156
- border: 1px solid transparent;
157
- border-radius: 3px;
158
- }
159
-
160
- .filters li a:hover {
161
- border-color: rgba(175, 47, 47, 0.1);
162
- }
163
-
164
- .filters li a.selected {
165
- border-color: rgba(175, 47, 47, 0.2);
166
- }
167
-
168
- .clear-completed,
169
- html .clear-completed:active {
170
- float: right;
171
- position: relative;
172
- line-height: 20px;
173
- text-decoration: none;
174
- cursor: pointer;
175
- }
176
-
177
- .clear-completed:hover {
178
- text-decoration: underline;
179
- }
180
-
181
- .info {
182
- margin: 65px auto 0;
183
- color: #bfbfbf;
184
- font-size: 10px;
185
- text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
186
- text-align: center;
187
- }
188
-
189
- .info p {
190
- line-height: 1;
191
- }
192
-
193
- .info a {
194
- color: inherit;
195
- text-decoration: none;
196
- font-weight: 400;
197
- }
198
-
199
- .info a:hover {
200
- text-decoration: underline;
201
- }
202
-
203
138
  @media screen and (-webkit-min-device-pixel-ratio: 0) {
204
139
  .toggle-all {
205
140
  background: none;
@@ -215,42 +150,4 @@ var _ = Css(`
215
150
  bottom: 10px;
216
151
  }
217
152
  }
218
-
219
- .todoapp {
220
- background: #fff;
221
- margin: 130px 0 40px 0;
222
- position: relative;
223
- box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
224
- }
225
-
226
- .todoapp input::-webkit-input-placeholder {
227
- font-style: italic;
228
- font-weight: 300;
229
- color: #e6e6e6;
230
- }
231
-
232
- .todoapp input::-moz-placeholder {
233
- font-style: italic;
234
- font-weight: 300;
235
- color: #e6e6e6;
236
- }
237
-
238
- .todoapp input::input-placeholder {
239
- font-style: italic;
240
- font-weight: 300;
241
- color: #e6e6e6;
242
- }
243
-
244
- .todoapp h1 {
245
- position: absolute;
246
- top: -155px;
247
- width: 100%;
248
- font-size: 100px;
249
- font-weight: 100;
250
- text-align: center;
251
- color: rgba(175, 47, 47, 0.15);
252
- -webkit-text-rendering: optimizeLegibility;
253
- -moz-text-rendering: optimizeLegibility;
254
- text-rendering: optimizeLegibility;
255
- }
256
153
  `)
cmd/gromer/main.go CHANGED
@@ -43,7 +43,7 @@ func getMethod(src string) string {
43
43
  } else if strings.HasSuffix(src, "trace.go") {
44
44
  return "TRACE"
45
45
  } else {
46
- panic(fmt.Sprintf("Uknown route found %s", src))
46
+ return ""
47
47
  }
48
48
  }
49
49
 
@@ -93,14 +93,17 @@ func main() {
93
93
  } else {
94
94
  moduleName = *pkgFlag
95
95
  }
96
- err := filepath.Walk("pages",
96
+ err := filepath.Walk("routes",
97
97
  func(filesrc string, info os.FileInfo, err error) error {
98
98
  if err != nil {
99
99
  return err
100
100
  }
101
101
  if !info.IsDir() {
102
- route := strings.ReplaceAll(filesrc, "pages", "")
102
+ route := strings.ReplaceAll(filesrc, "routes", "")
103
103
  method := getMethod(route)
104
+ if method == "" {
105
+ return nil
106
+ }
104
107
  path := getRoute(method, route)
105
108
  if path == "" { // for index page
106
109
  path = "/"
@@ -191,21 +194,22 @@ package main
191
194
  import (
192
195
  "github.com/gorilla/mux"
193
196
  "github.com/pyros2097/gromer"
197
+ "github.com/pyros2097/gromer/gsx"
194
198
  "github.com/rs/zerolog/log"
195
199
  "gocloud.dev/server"
196
200
 
197
201
  "{{ moduleName }}/assets"
198
202
  "{{ moduleName }}/components"
199
203
  "{{ moduleName }}/containers"
200
- {{#if notFoundPkg}}"{{ moduleName }}/pages/404"{{/if}}
204
+ {{#if notFoundPkg}}"{{ moduleName }}/routes/404"{{/if}}
201
- {{#each routeImports as |route| }}"{{ moduleName }}/pages{{ route.PkgPath }}"
205
+ {{#each routeImports as |route| }}"{{ moduleName }}/routes{{ route.PkgPath }}"
202
206
  {{/each}}
203
207
  )
204
208
 
205
209
  func init() {
206
- {{#each componentNames as |name| }}gromer.RegisterComponent(components.{{ name }})
210
+ {{#each componentNames as |name| }}gsx.RegisterComponent(components.{{ name }})
207
211
  {{/each}}
208
- {{#each containerNames as |name| }}gromer.RegisterContainer(containers.{{ name }})
212
+ {{#each containerNames as |name| }}gsx.RegisterComponent(containers.{{ name }})
209
213
  {{/each}}
210
214
  gromer.RegisterAssets(assets.FS)
211
215
  }
@@ -223,7 +227,6 @@ func main() {
223
227
  gromer.StylesRoute(staticRouter, "/styles.css")
224
228
 
225
229
  pageRouter := baseRouter.NewRoute().Subrouter()
226
- gromer.ApiExplorerRoute(pageRouter, "/explorer")
227
230
  {{#each pageRoutes as |route| }}gromer.Handle(pageRouter, "{{ route.Method }}", "{{ route.Path }}", {{ route.Pkg }}.{{ route.Method }})
228
231
  {{/each}}
229
232
 
readme.md CHANGED
@@ -2,11 +2,12 @@
2
2
 
3
3
  [![Version](https://badge.fury.io/gh/pyros2097%2Fgromer.svg)](https://github.com/pyros2097/gromer)
4
4
 
5
- **gromer** is a framework and cli to build web apps in golang.
5
+ **gromer** is a framework and cli to build multipage web apps in golang using [htmx](https://htmx.org/) and [alpinejs](https://alpinejs.dev/).
6
- It uses a declarative syntax using inline templates for components and pages.
6
+ It uses a declarative syntax using inline jsx like templates for components and pages.
7
7
  It also generates http handlers for your routes which follow a particular folder structure. Similar to other frameworks like nextjs, sveltekit.
8
- These handlers are also normal functions and can be imported in other packages directly. ((inspired by [Encore](https://encore.dev/)).
8
+
9
- More information on the templating syntax is given [here](https://github.com/pyrossh/gromer/blob/master/handlebars/README.md),
9
+ You can install this extension [vscode-go-inline-html](https://marketplace.visualstudio.com/items?itemName=pyros2097.vscode-go-inline-html) to get
10
+ syntax highlighting for these templates.
10
11
 
11
12
  # Requirements
12
13
 
@@ -26,140 +27,93 @@ You need to follow this directory structure similar to nextjs for the api route
26
27
 
27
28
  [Example](https://github.com/pyros2097/gromer/tree/master/_example)
28
29
 
30
+ **These are some components**
29
31
 
30
- **These are normal page routes**
31
- ```go
32
- // /pages/get.go
32
+ `routes/todo.go`
33
- package todos_page
34
-
35
- import (
36
- "context"
37
-
38
- . "github.com/pyros2097/gromer"
39
- _ "github.com/pyros2097/gromer/_example/components"
40
- "github.com/pyros2097/gromer/_example/pages/api/todos"
41
- . "github.com/pyros2097/gromer/handlebars"
42
- )
43
-
44
- type GetParams struct {
45
- Filter string `json:"filter"`
46
- Page int `json:"limit"`
47
- }
48
33
 
49
- func GET(ctx context.Context, params GetParams) (HtmlContent, int, error) {
50
- index := Default(params.Page, 1)
34
+ ```go
51
- todos, status, err := todos.GET(ctx, todos.GetParams{
35
+ func Todo(c Context, todo *todos.Todo) *Node {
52
- Filter: params.Filter,
53
- Limit: index * 10,
54
- })
55
- if err != nil {
56
- return HtmlErr(status, err)
57
- }
58
- return Html(`
36
+ return c.Render(`
59
- <Page title="gromer example">
60
- <Header></Header>
61
- <section class="todoapp">
37
+ <li id="todo-{todo.ID}" class="{ completed: todo.Completed }">
62
- <section class="main">
38
+ <div class="view">
39
+ <form hx-target="#todo-{todo.ID}" hx-swap="outerHTML">
40
+ <input type="hidden" name="intent" value="complete" />
41
+ <input type="hidden" name="id" value="{todo.ID}" />
42
+ <input class="checkbox" type="checkbox" checked="{value}" />
43
+ </form>
44
+ <label>{todo.Text}</label>
45
+ <form hx-post="/" hx-target="#todo-{todo.ID}" hx-swap="delete">
46
+ <input type="hidden" name="intent" value="delete" />
47
+ <input type="hidden" name="id" value="{todo.ID}" />
63
- <ul class="todo-list" id="todo-list">
48
+ <button class="destroy"></button>
64
- {{#each todos as |todo|}}
65
- {{#Todo todo=todo}}{{/Todo}}
49
+ </form>
66
- {{/each}}
50
+ </div>
67
- </ul>
51
+ </li>
68
- </section>
69
- {{/if}}
70
- </section>
71
- </Page>
72
- `).
52
+ `)
73
- Prop("todos", todos).
74
- Render()
75
53
  }
76
54
  ```
77
55
 
56
+ **These are normal page routes**
78
57
 
79
- **These are API routes**
80
- ```go
81
- // /pages/api/todos/get.go
82
- package todos
83
-
84
- import (
85
- "context"
58
+ `routes/get.go`
86
-
87
- . "github.com/pyros2097/gromer"
88
- "github.com/pyros2097/gromer/_example/services"
89
- )
90
59
 
60
+ ```go
91
61
  type GetParams struct {
92
- Limit int `json:"limit"`
62
+ Page int `json:"page"`
93
63
  Filter string `json:"filter"`
94
64
  }
95
65
 
96
- func GET(ctx context.Context, params GetParams) ([]*services.Todo, int, error) {
66
+ func GET(c Context, params GetParams) (*Node, int, error) {
97
- limit := Default(params.Limit, 10)
67
+ c.Meta("title", "Gromer Todos")
98
- todos := services.GetAllTodo(ctx, services.GetAllTodoParams{
68
+ c.Meta("description", "Gromer Todos")
69
+ c.Meta("author", "gromer")
70
+ c.Meta("keywords", "gromer")
71
+ return c.Render(`
72
+ <div class="todoapp">
73
+ <header class="header">
74
+ <h1>todos</h1>
75
+ <form hx-post="/" hx-target="#todo-list" hx-swap="afterbegin" _="on htmx:afterOnLoad set #text.value to ''">
76
+ <input type="hidden" name="intent" value="create" />
77
+ <input class="new-todo" id="text" name="text" placeholder="What needs to be done?" autofocus="false" autocomplete="off" />
99
- Limit: limit,
78
+ </form>
100
- })
79
+ </header>
80
+ <section class="main">
81
+ <input class="toggle-all" id="toggle-all" type="checkbox" />
82
+ <label for="toggle-all">Mark all as complete</label>
83
+ <TodoList id="todo-list" page="{params.Page}" filter="{params.Filter}" />
84
+ </section>
101
- if params.Filter == "completed" {
85
+ <footer class="footer">
102
- newTodos := []*services.Todo{}
86
+ <TodoCount filter="{params.Filter}" />
103
- for _, v := range todos {
104
- if v.Completed {
105
- newTodos = append(newTodos, v)
106
- }
107
- }
108
- return newTodos, 200, nil
109
- }
110
- if params.Filter == "active" {
87
+ <ul class="filters">
88
+ <li>
111
- newTodos := []*services.Todo{}
89
+ <a href="?filter=all">All</a>
112
- for _, v := range todos {
113
- if !v.Completed {
90
+ </li>
91
+ <li>
92
+ <a href="?filter=active">Active</a>
93
+ </li>
94
+ <li>
95
+ <a href="?filter=completed">Completed</a>
96
+ </li>
97
+ </ul>
98
+ <form hx-target="#todo-list" hx-post="/">
99
+ <input type="hidden" name="intent" value="clear_completed" />
100
+ <button type="submit" class="clear-completed" >Clear completed</button>
114
- newTodos = append(newTodos, v)
101
+ </form>
115
- }
116
- }
117
- return newTodos, 200, nil
102
+ </footer>
118
- }
103
+ </div>
119
- return todos, 200, nil
104
+ `), 200, nil
120
105
  }
121
-
122
106
  ```
123
107
 
124
- ```go
125
- // /pages/api/todos/post.go
126
- package todos
127
-
128
- import (
129
- "context"
130
- "time"
131
-
132
- "github.com/google/uuid"
133
- "github.com/pyros2097/gromer/_example/services"
108
+ And then run the gromer cli command annd it will generate the route handlers in a main.go file,
134
- )
135
-
136
- type PostParams struct {
137
- Text string `json:"text"`
138
- }
139
109
 
140
- func POST(ctx context.Context, b PostParams) (*services.Todo, int, error) {
141
- todo, err := services.CreateTodo(ctx, services.Todo{
142
- ID: uuid.New().String(),
143
- Text: b.Text,
144
- Completed: false,
145
- CreatedAt: time.Now(),
110
+ `main.go`
146
- UpdatedAt: time.Now(),
147
- })
148
- if err != nil {
149
- return nil, 500, err
150
- }
151
- return todo, 200, nil
152
- }
153
- ```
154
111
 
155
- And then run the gromer cli command annd it will generate the route handlers in a main.go file,
156
112
  ```go
157
113
  // Code generated by gromer. DO NOT EDIT.
158
114
  package main
159
115
 
160
116
  import (
161
- "os"
162
-
163
117
  "github.com/gorilla/mux"
164
118
  "github.com/pyros2097/gromer"
165
119
  "github.com/rs/zerolog/log"
@@ -167,77 +121,57 @@ import (
167
121
 
168
122
  "github.com/pyros2097/gromer/_example/assets"
169
123
  "github.com/pyros2097/gromer/_example/components"
124
+ "github.com/pyros2097/gromer/_example/containers"
170
- "github.com/pyros2097/gromer/_example/pages/404"
125
+ "github.com/pyros2097/gromer/_example/routes/404"
171
- "github.com/pyros2097/gromer/_example/pages"
126
+ "github.com/pyros2097/gromer/_example/routes"
172
- "github.com/pyros2097/gromer/_example/pages/about"
127
+ "github.com/pyros2097/gromer/_example/routes/about"
173
- "github.com/pyros2097/gromer/_example/pages/api/recover"
174
- "github.com/pyros2097/gromer/_example/pages/api/todos"
128
+ "github.com/pyros2097/gromer/gsx"
175
- "github.com/pyros2097/gromer/_example/pages/api/todos/_todoId_"
129
+
176
-
177
130
  )
178
131
 
179
132
  func init() {
180
- gromer.RegisterComponent(components.Header)
181
- gromer.RegisterComponent(components.Page)
182
- gromer.RegisterComponent(components.Todo)
133
+ gsx.RegisterComponent(components.Todo, "todo")
183
-
134
+ gsx.RegisterComponent(components.Checkbox, "value")
135
+
136
+ gsx.RegisterComponent(containers.TodoCount, "filter")
137
+ gsx.RegisterComponent(containers.TodoList, "page", "filter")
138
+ gromer.RegisterAssets(assets.FS)
184
139
  }
185
140
 
186
141
  func main() {
187
- port := os.Getenv("PORT")
188
142
  baseRouter := mux.NewRouter()
189
143
  baseRouter.Use(gromer.LogMiddleware)
190
-
144
+
191
145
  baseRouter.NotFoundHandler = gromer.StatusHandler(not_found_404.GET)
192
-
146
+
193
147
  staticRouter := baseRouter.NewRoute().Subrouter()
194
148
  staticRouter.Use(gromer.CacheMiddleware)
149
+ gromer.GromerRoute(staticRouter, "/gromer/")
195
- gromer.StaticRoute(staticRouter, "/assets/", assets.FS)
150
+ gromer.StaticRoute(staticRouter, "/assets/")
196
151
  gromer.StylesRoute(staticRouter, "/styles.css")
197
152
 
198
153
  pageRouter := baseRouter.NewRoute().Subrouter()
199
- gromer.ApiExplorerRoute(pageRouter, "/explorer")
154
+ // gromer.ApiExplorerRoute(pageRouter, "/explorer")
200
- gromer.Handle(pageRouter, "GET", "/", pages.GET)
155
+ gromer.Handle(pageRouter, "GET", "/", routes.GET)
156
+ gromer.Handle(pageRouter, "POST", "/", routes.POST)
201
157
  gromer.Handle(pageRouter, "GET", "/about", about.GET)
202
-
158
+
203
159
 
204
160
  apiRouter := baseRouter.NewRoute().Subrouter()
205
161
  apiRouter.Use(gromer.CorsMiddleware)
206
- gromer.Handle(apiRouter, "GET", "/api/recover", recover.GET)
162
+
207
- gromer.Handle(apiRouter, "GET", "/api/todos", todos.GET)
163
+
208
- gromer.Handle(apiRouter, "POST", "/api/todos", todos.POST)
164
+
209
- gromer.Handle(apiRouter, "DELETE", "/api/todos/{todoId}", todos_todoId_.DELETE)
210
- gromer.Handle(apiRouter, "GET", "/api/todos/{todoId}", todos_todoId_.GET)
211
- gromer.Handle(apiRouter, "PUT", "/api/todos/{todoId}", todos_todoId_.PUT)
212
-
213
-
214
-
215
- log.Info().Msg("http server listening on http://localhost:"+port)
165
+ log.Info().Msg("http server listening on http://localhost:3000")
216
166
  srv := server.New(baseRouter, nil)
217
- if err := srv.ListenAndServe(":"+port); err != nil {
167
+ if err := srv.ListenAndServe(":3000"); err != nil {
218
168
  log.Fatal().Stack().Err(err).Msg("failed to listen")
219
169
  }
220
170
  }
221
171
  ```
222
172
 
223
173
  ## TODO:
174
+
224
175
  Add inline css formatting
176
+
225
- ADd inline html formatting
177
+ Add inline html formatting
226
-
227
- ## Ideas:
228
- ```js
229
- <script>
230
- document.addEventListener('alpine:init', () => {
231
- Alpine.store('todos', {
232
- list: [],
233
- count: 0,
234
- })
235
- })
236
- </script>
237
-
238
- // Send patches in all Post API's instead of data
239
- [
240
- { "op": "add", "path": "/todos/list-", "value": { "id": "123", "text": "123" } },
241
- { "op": "replace", "path": "/todos/count", "value": 1 } ,
242
- ]
243
- ```