diff --git a/data/url/beforeSend-all.html b/data/url/beforeSend-all.html new file mode 100644 index 00000000..057c9f79 --- /dev/null +++ b/data/url/beforeSend-all.html @@ -0,0 +1,69 @@ +<person-list></person-list> + +<script type="module"> +import { + fixture, + restModel, + ObservableObject, + StacheElement, +} from "can/ecosystem"; + +setupFixtures(); + +class Person extends ObservableObject { + static props = { + firstName: String, + lastName: String, + age: Number, + }; +} + +const personConnection = restModel({ + Map: Person, + url: { + resource: '/api/person', + beforeSend(xhr) { + xhr.setRequestHeader('Authorization', 'Bearer: some-authorization-token'); + } + } +}); + +class PersonList extends StacheElement { + static view =` + <button on:click="this.getList()">Make request for person list</button> + <ul> + {{#for(person of this.people)}} + <li>{{person.firstName}} {{person.lastName}} - {{person.age}}</li> + {{else}} + <li>No people loaded.</li> + {{/for}} + </ul> + `; + + static props = { + people: { + get default() { return []; } + } + }; + + getList() { + personConnection.getList().then((list) => {; + this.people = list; + }); + } +} +customElements.define("person-list", PersonList); + +function setupFixtures() { + fixture("GET /api/person", function (request, response) { + if (request.headers.Authorization) { // validate auth header + response([ + {firstName: 'Keanu', lastName: 'Reeves', age: 55}, + {firstName: 'Tom', lastName: 'Cruise', age: 57}, + ]); + } else { + response(401, {message: "Unauthorized"}, {}, "unauthorized"); + } + }); +} +</script> diff --git a/data/url/custom-request.html b/data/url/custom-request.html new file mode 100644 index 00000000..a05c52f1 --- /dev/null +++ b/data/url/custom-request.html @@ -0,0 +1,89 @@ +<todo-list></todo-list> + +<script type="module"> + import { + ajax, + fixture, + restModel, + ObservableObject, + StacheElement, + } from "can/ecosystem"; + + setupFixtures(); + + class Todo extends ObservableObject { + static props = { + id: Number, + content: String, + complete: Boolean, + }; + } + + const todoConnection = restModel({ + Map: Todo, + url: { + resource: "/services/todo", + getListData: "GET /services/todos", + getData: function(param){ + return ajax({ + url: '/services/todo', + data: { identifier: param.id } + }); + } + } + }); + + class TodoList extends StacheElement { + static view =` + <button on:click="this.getList()">Make request for Todo list</button> + <ul> + {{#for(todo of this.todos)}} + <li on:click="this.getTodo(todo.id)">{{todo.id}}</li> + {{else}} + <li>No todos loaded.</li> + {{/for}} + </ul> + + {{#if this.activeTodo}} + <h3>Active Todo:</h3> + <p>{{this.activeTodo.content}} - <input type="checkbox" disabled checked:from="{{this.activeTodo.complete}}"></p> + {{/if}} + `; + + static props = { + todos: { + get default() { return []; } + }, + activeTodo: Todo + }; + + getList() { + todoConnection.getList().then((list) => { + this.todos = list; + }); + } + + getTodo(id) { + todoConnection.get({id}).then((todo) => { + this.activeTodo = todo; + }) + } + } + customElements.define("todo-list", TodoList); + + function setupFixtures() { + const todos = [ + {id: 1, content: 'Do the dishes', complete: true}, + {id: 2, content: 'Walk the dog', complete: false}, + {id: 3, content: 'Sweep the floor', complete: false}, + ]; + + fixture("GET /services/todos", function (request, response) { + response(todos); + }); + + fixture("GET /services/todo", function (request, response) { + response(todos[request.data.identifier - 1]); + }) + } +</script> diff --git a/data/url/plain-endpoints.html b/data/url/plain-endpoints.html new file mode 100644 index 00000000..e96bf0c5 --- /dev/null +++ b/data/url/plain-endpoints.html @@ -0,0 +1,96 @@ +<request-demo></request-demo> + +<script type="module"> +import { + fixture, + restModel, + ObservableObject, + StacheElement, +} from "can/ecosystem"; + +setupFixtures(); + +class Todo extends ObservableObject { + static props = { + id: Number, + content: String, + complete: Boolean, + }; +} + +const todoConnection = restModel({ + Map: Todo, + url: { + getListData: "GET /services/todos", + getData: "GET /services/todo/{id}", + createData: "POST /services/todo", + updateData: "PUT /services/todo/{id}", + destroyData: "DELETE /services/todo/{id}" + } +}); + +class RequestDemo extends StacheElement { + static view =` + <button on:click="this.getList()">Todo list request</button> + <button on:click="this.get()">Single todo request</button> + <button on:click="this.create()">Todo creation request</button> + <button on:click="this.update()">Todo update request</button> + <button on:click="this.delete()">Todo deletion request</button> + `; + + getList() { + todoConnection.getList(); + } + + get() { + todoConnection.get({id: 5}); + } + + create() { + new Todo({ + content: 'New todo', + complete: false, + }).save(); + } + + update() { + new Todo({ + id: 1, + content: 'Old todo', + complete: true, + }).save(); // makes an update request since this instance already has an id + } + + delete() { + new Todo({ + id: 1, + content: 'Old todo', + complete: true, + }).destroy(); + } +} +customElements.define("request-demo", RequestDemo); + +function setupFixtures() { + fixture("GET /services/todos", function (request, response) { + console.log('Todo list request'); + response([]); + }); + fixture("GET /services/todo/{id}", function (request, response) { + console.log(`Todo id ${request.data.id} request`); + response({}); + }); + fixture("POST /services/todo", function (request, response) { + console.log(`Todo created`); + response({}); + }); + fixture("PUT /services/todo/{id}", function (request, response) { + console.log(`Todo id ${request.data.id} updated`); + response({}); + }); + fixture("DELETE /services/todo/{id}", function (request, response) { + console.log(`Todo id ${request.data.id} deleted`); + response({}); + }); +} +</script> diff --git a/data/url/request-params.html b/data/url/request-params.html new file mode 100644 index 00000000..1a363880 --- /dev/null +++ b/data/url/request-params.html @@ -0,0 +1,85 @@ +<todo-list></todo-list> + +<script type="module"> + import { + ajax, + fixture, + restModel, + ObservableObject, + StacheElement, + } from "can/ecosystem"; + + setupFixtures(); + + class Todo extends ObservableObject { + static props = { + id: Number, + content: String, + complete: Boolean, + }; + } + + function processContext() { + // react to the response of the '/services/context' request before sending getListData request + } + + const todoConnection = restModel({ + Map: Todo, + url: { + resource: "/services/todos", + getListData: { + url: "/services/todos", + type: "GET", + beforeSend: () => { + return ajax({url: '/services/context'}).then(processContext); + } + } + } + }); + + class TodoList extends StacheElement { + static view =` + <button on:click="this.getList()">Make request for Todo list</button> + <ul> + {{#for(todo of this.todos)}} + <li> + <input type="checkbox" disabled checked:from="{{todo.complete}}"> + {{todo.id}} - {{todo.content}} + </li> + {{else}} + <li>No todos loaded.</li> + {{/for}} + </ul> + `; + + static props = { + todos: { + get default() { return []; } + }, + }; + + getList() { + todoConnection.getList().then((list) => { + this.todos = list; + }); + } + } + customElements.define("todo-list", TodoList); + + function setupFixtures() { + const todos = [ + {id: 1, content: 'Do the dishes', complete: true}, + {id: 2, content: 'Walk the dog', complete: false}, + {id: 3, content: 'Sweep the floor', complete: false}, + ]; + + fixture("GET /services/todos", function (request, response) { + response(todos); + }); + + // arbitrary preliminary request + fixture("GET /services/context", function (request, response) { + response({}); + }); + } +</script> diff --git a/data/url/resource-param.html b/data/url/resource-param.html new file mode 100644 index 00000000..9fbe75e9 --- /dev/null +++ b/data/url/resource-param.html @@ -0,0 +1,83 @@ +<todo-list></todo-list> + +<script type="module"> + import { + ajax, + fixture, + restModel, + ObservableObject, + StacheElement, + } from "can/ecosystem"; + + setupFixtures(); + + class Todo extends ObservableObject { + static props = { + id: Number, + content: String, + complete: Boolean, + }; + } + + const todoConnection = restModel({ + Map: Todo, + url: { + resource: "/services/todo", + getListData: "GET /services/todos", + } + }); + + class TodoList extends StacheElement { + static view =` + <button on:click="this.getList()">Make request for Todo list</button> + <ul> + {{#for(todo of this.todos)}} + <li on:click="this.getTodo(todo.id)">{{todo.id}}</li> + {{else}} + <li>No todos loaded.</li> + {{/for}} + </ul> + + {{#if this.activeTodo}} + <h3>Active Todo:</h3> + <p>{{this.activeTodo.content}} - <input type="checkbox" disabled checked:from="{{this.activeTodo.complete}}"></p> + {{/if}} + `; + + static props = { + todos: { + get default() { return []; } + }, + activeTodo: Todo + }; + + getList() { + todoConnection.getList().then((list) => { + this.todos = list; + }); + } + + getTodo(id) { + todoConnection.get({id}).then((todo) => { + this.activeTodo = todo; + }) + } + } + customElements.define("todo-list", TodoList); + + function setupFixtures() { + const todos = [ + {id: 1, content: 'Do the dishes', complete: true}, + {id: 2, content: 'Walk the dog', complete: false}, + {id: 3, content: 'Sweep the floor', complete: false}, + ]; + + fixture("GET /services/todos", function (request, response) { + response(todos); + }); + + fixture("GET /services/todo/{id}", function (request, response) { + response(todos[parseInt(request.data.id) - 1]); + }) + } +</script> diff --git a/data/url/url.js b/data/url/url.js index 5a81a2ec..10fa2bd1 100644 --- a/data/url/url.js +++ b/data/url/url.js @@ -159,93 +159,6 @@ var urlBehavior = behavior("data/url", function(baseConnection) { return behavior; }); -/** - * @property {String|Object} can-connect/data/url/url.url url - * @parent can-connect/data/url/url.option - * - * Specify the url and methods that should be used for the "Data Methods". - * - * @option {String} If a string is provided, it's assumed to be a RESTful interface. For example, - * if the following is provided: - * - * ``` - * url: "/services/todos" - * ``` - * - * ... the following methods and requests are used: - * - * - `getListData` - `GET /services/todos` - * - `getData` - `GET /services/todos/{id}` - * - `createData` - `POST /services/todos` - * - `updateData` - `PUT /services/todos/{id}` - * - `destroyData` - `DELETE /services/todos/{id}` - * - * @option {Object} If an object is provided, it can customize each method and URL directly - * like: - * - * ```js - * url: { - * getListData: "GET /services/todos", - * getData: "GET /services/todo/{id}", - * createData: "POST /services/todo", - * updateData: "PUT /services/todo/{id}", - * destroyData: "DELETE /services/todo/{id}" - * } - * ``` - * - * You can provide a `resource` property that works like providing `url` as a string, but overwrite - * other values like: - * - * ```js - * url: { - * resource: "/services/todo", - * getListData: "GET /services/todos" - * } - * ``` - * - * You can also customize per-method the parameters passed to the [can-connect/data/url/url.ajax ajax implementation], like: - * ```js - * url: { - * resource: "/services/todos", - * getListData: { - * url: "/services/todos", - * type: "GET", - * beforeSend: () => { - * return fetch('/services/context').then(processContext); - * } - * } - * } - * ``` - * This can be particularly useful for passing a handler for the [can-ajax <code>beforeSend</code>] hook. - * - * The [can-ajax <code>beforeSend</code>] hook can also be passed for all request methods. This can be useful when - * attaching a session token header to a request: - * - * ```js - * url: { - * resource: "/services/todos", - * beforeSend: (xhr) => { - * xhr.setRequestHeader('Authorization', `Bearer: ${Session.current.token}`); - * } - * } - * ``` - * - * Finally, you can provide your own method to totally control how the request is made: - * - * ```js - * url: { - * resource: "/services/todo", - * getListData: "GET /services/todos", - * getData: function(param){ - * return new Promise(function(resolve, reject){ - * $.get("/services/todo", {identifier: param.id}).then(resolve, reject); - * }); - * } - * } - * ``` - */ - - /** * @property {function} can-connect/data/url/url.ajax ajax * @parent can-connect/data/url/url.option diff --git a/data/url/url.url.md b/data/url/url.url.md new file mode 100644 index 00000000..1c5b4009 --- /dev/null +++ b/data/url/url.url.md @@ -0,0 +1,53 @@ +@property {String|Object} can-connect/data/url/url.url url +@parent can-connect/data/url/url.option + +Specify the url and methods that should be used for the "Data Methods". + +@option {String} If a string is provided, it's assumed to be a RESTful interface. For example, +if the following is provided: + +``` +url: "/services/todos" +``` + +... the following methods and requests are used: + + - `getListData` - `GET /services/todos` + - `getData` - `GET /services/todos/{id}` + - `createData` - `POST /services/todos` + - `updateData` - `PUT /services/todos/{id}` + - `destroyData` - `DELETE /services/todos/{id}` + +@option {Object} If an object is provided, it can customize each method and URL directly +like: + + @sourceref ./plain-endpoints.html + @highlight 23-29,only + @codepen + +You can provide a `resource` property that works like providing `url` as a string, but overwrite +other values like: + + @sourceref ./resource-param.html + @highlight 25,only + @codepen + +You can also customize per-method the parameters passed to the [can-connect/data/url/url.ajax ajax implementation], like: + @sourceref ./request-params.html + @highlight 30-36,only + @codepen +This can be particularly useful for passing a handler for the [can-ajax <code>beforeSend</code>] hook. + +<a id="beforeSend"></a> +The [can-ajax <code>beforeSend</code>] hook can also be passed for all request methods. This can be useful when +attaching a session token header to a request: + + @sourceref ./beforeSend-all.html + @highlight 25-27,only + @codepen + +Finally, you can provide your own method to totally control how the request is made: + + @sourceref ./custom-request.html + @highlight 27-32,only + @codepen \ No newline at end of file