{"html_url": "https://github.com/simonw/datasette/issues/987#issuecomment-752696499", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/987", "id": 752696499, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MjY5NjQ5OQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T17:21:08Z", "updated_at": "2020-12-30T17:21:08Z", "author_association": "OWNER", "body": "More generally, I need to document certain areas of the page that JavaScript plugins are invited to append their content to - such that plugin authors can use them and feel confident that future changes to the Datasette templates won't break their plugins.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712984738, "label": "Documented HTML hooks for JavaScript plugin authors"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/987#issuecomment-752697279", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/987", "id": 752697279, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MjY5NzI3OQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T17:23:27Z", "updated_at": "2020-12-30T17:23:32Z", "author_association": "OWNER", "body": "Related problem: right now if you're writing custom template it's not at all obvious how to write them such that visualization plugins like `datasette-cluster-map` will continue to work.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712984738, "label": "Documented HTML hooks for JavaScript plugin authors"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/987#issuecomment-752714747", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/987", "id": 752714747, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MjcxNDc0Nw==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T18:23:08Z", "updated_at": "2020-12-30T18:23:20Z", "author_association": "OWNER", "body": "In terms of \"places to put your plugin content\", the simplest solution I can think of is something like this:\r\n```html\r\n
\r\n```\r\nAlternative designs:\r\n\r\n- A documented JavaScript function that returns the CSS selector where plugins should put their content\r\n- A documented JavaScript function that returns a DOM node where plugins should put their content. This would allow the JavaScript to create the element if it does not already exist (though it wouldn't be obvious WHERE that element should be created)\r\n- Documented JavaScript functions for things like \"append this node/HTML to the place-where-plugins-go\"\r\n\r\nI think the original option - an empty `
` with a known `id` attribute - is the right one to go with here. It's the simplest, it's very easy for custom template authors to understand and it acknowledges that plugins may have all kinds of extra crazy stuff they want to do - like checking in that div to see if another plugin has written to it already, for example.", "reactions": "{\"total_count\": 1, \"+1\": 1, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712984738, "label": "Documented HTML hooks for JavaScript plugin authors"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-752715236", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752715236, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MjcxNTIzNg==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T18:24:54Z", "updated_at": "2020-12-30T18:24:54Z", "author_association": "OWNER", "body": "I think I'm going to try building a very lightweight clone of the core API design of Pluggy - not the advanced features, just the idea that plugins can register and a call to `plugin.nameOfHook()` will return the concatenated results of all of the registered hooks.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712260429, "label": "JavaScript plugin hooks mechanism similar to pluggy"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-752715412", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752715412, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MjcxNTQxMg==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T18:25:31Z", "updated_at": "2020-12-30T18:25:31Z", "author_association": "OWNER", "body": "I'm going to introduce a global `datasette` object which holds all the documented JavaScript API for plugin authors.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712260429, "label": "JavaScript plugin hooks mechanism similar to pluggy"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-752721069", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752721069, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MjcyMTA2OQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T18:46:10Z", "updated_at": "2020-12-30T18:46:10Z", "author_association": "OWNER", "body": "Pluggy does dependency injection by introspecting the named arguments to the Python function, which I really like.\r\n\r\nThat's tricker in JavaScript. It looks like the only way to introspect a function is to look at the `.toString()` representation of it and parse the `(parameter, list)` using a regular expression.\r\n\r\nEven more challenging: JavaScript developers love minifying their code, and minification can shorten the function parameter names.\r\n\r\nFrom https://code-maven.com/dependency-injection-in-angularjs it looks like Angular.js does dependency injection and solves this by letting you optionally provide a separate list of the arguments your function uses:\r\n\r\n```javascript\r\n angular.module('DemoApp', [])\r\n .controller('DemoController', ['$scope', '$log', function($scope, $log) {\r\n $scope.message = \"Hello World\";\r\n $log.debug('logging hello');\r\n }]);\r\n```\r\n\r\nI can copy that approach: I'll introspect by default, but provide a documented mechanism for explicitly listing your parameter names so that if you know your plugin code will be minified you can use that instead.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712260429, "label": "JavaScript plugin hooks mechanism similar to pluggy"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-752721840", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752721840, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MjcyMTg0MA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T18:48:53Z", "updated_at": "2020-12-30T18:51:51Z", "author_association": "OWNER", "body": "Potential design:\r\n```javascript\r\ndatasette.plugins.register('column_actions', function(database, table, column, actor) {\r\n /* ... *l\r\n})\r\n```\r\nOr if you want to be explicit to survive minification:\r\n```javascript\r\ndatasette.plugins.register('column_actions', function(database, table, column, actor) {\r\n /* ... *l\r\n}, ['database', 'table', 'column', 'actor'])\r\n```\r\nI'm making that list of parameter names an optional third argument to the `register()` function. If that argument isn't passed, introspection will be used to figure out the parameter names.\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712260429, "label": "JavaScript plugin hooks mechanism similar to pluggy"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-752722863", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752722863, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MjcyMjg2Mw==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T18:52:39Z", "updated_at": "2020-12-30T18:52:39Z", "author_association": "OWNER", "body": "Then to call the plugins:\r\n```javascript\r\ndatasette.plugins.call('column_actions', {database: 'database', table: 'table'})\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712260429, "label": "JavaScript plugin hooks mechanism similar to pluggy"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-752729035", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752729035, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MjcyOTAzNQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T19:15:56Z", "updated_at": "2020-12-30T19:16:44Z", "author_association": "OWNER", "body": "The `column_actions` hook is the obvious first place to try this out. What are some demo plugins I could build for it?\r\n\r\n- Word cloud for this column\r\n- Count values (essentially linking to the SQL query for that column, as an extended version of the facet counts) - would be great if this could include pagination somehow, via #856.\r\n- Extract this column into a separate table\r\n- Add an index to this column", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712260429, "label": "JavaScript plugin hooks mechanism similar to pluggy"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-752742669", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752742669, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc0MjY2OQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T20:07:05Z", "updated_at": "2020-12-30T20:07:18Z", "author_association": "OWNER", "body": "Initial prototype:\r\n```javascript\r\nwindow.datasette = {};\r\nwindow.datasette.plugins = (function() {\r\n var registry = {};\r\n\r\n function extractParameters(fn) {\r\n var match = /\\((.*)\\)/.exec(fn.toString());\r\n if (match && match[1].trim()) {\r\n return match[1].split(',').map(s => s.trim());\r\n } else {\r\n return [];\r\n }\r\n }\r\n\r\n function register(hook, fn, parameters) {\r\n parameters = parameters || extractParameters(fn);\r\n if (!registry[hook]) {\r\n registry[hook] = [];\r\n }\r\n registry[hook].push([fn, parameters]);\r\n }\r\n\r\n function call(hook, args) {\r\n args = args || {};\r\n var implementations = registry[hook] || [];\r\n var results = [];\r\n implementations.forEach(([fn, parameters]) => {\r\n /* Call with the correct arguments */\r\n var callWith = parameters.map(parameter => args[parameter]);\r\n var result = fn.apply(fn, callWith);\r\n if (result) {\r\n results.push(result);\r\n }\r\n });\r\n return results;\r\n }\r\n return {\r\n register: register,\r\n _registry: registry,\r\n call: call\r\n };\r\n})();\r\n```\r\nUsage example:\r\n```javascript\r\ndatasette.plugins.register('numbers', (a, b) => a + b)\r\ndatasette.plugins.register('numbers', (a, b) => a * b)\r\ndatasette.plugins.call('numbers', {a: 4, b: 6})\r\n/* Returns [10, 24] */\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712260429, "label": "JavaScript plugin hooks mechanism similar to pluggy"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-752744195", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752744195, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc0NDE5NQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T20:12:26Z", "updated_at": "2020-12-30T20:12:26Z", "author_association": "OWNER", "body": "This implementation doesn't have an equivalent of \"hookspecs\" which can identify if a registered plugin implementation matches a known signature. I should add that, it will provide a better developer experience if someone has a typo.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712260429, "label": "JavaScript plugin hooks mechanism similar to pluggy"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-752744311", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752744311, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc0NDMxMQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T20:12:50Z", "updated_at": "2020-12-30T20:13:02Z", "author_association": "OWNER", "body": "This could work to define a plugin hook:\r\n```javascript\r\ndatasette.plugins.define('numbers', ['a' ,'b'])\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712260429, "label": "JavaScript plugin hooks mechanism similar to pluggy"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-752747169", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752747169, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc0NzE2OQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T20:24:07Z", "updated_at": "2020-12-30T20:24:07Z", "author_association": "OWNER", "body": "This version adds `datasette.plugins.define()` plus extra validation of both `.register()` and `.call()`:\r\n```javascript\r\nwindow.datasette = {};\r\nwindow.datasette.plugins = (function() {\r\n var registry = {};\r\n var definitions = {};\r\n\r\n function extractParameters(fn) {\r\n var match = /\\((.*)\\)/.exec(fn.toString());\r\n if (match && match[1].trim()) {\r\n return match[1].split(',').map(s => s.trim());\r\n } else {\r\n return [];\r\n }\r\n }\r\n\r\n function define(hook, parameters) {\r\n definitions[hook] = parameters || [];\r\n }\r\n\r\n function isSubSet(a, b) {\r\n return a.every(parameter => b.includes(parameter))\r\n }\r\n\r\n function register(hook, fn, parameters) {\r\n parameters = parameters || extractParameters(fn);\r\n if (!definitions[hook]) {\r\n throw new Error('\"' + hook + '\" is not a defined plugin hook');\r\n }\r\n if (!definitions[hook]) {\r\n throw new Error('\"' + hook + '\" is not a defined plugin hook');\r\n }\r\n /* Check parameters is a subset of definitions[hook] */\r\n var validParameters = definitions[hook];\r\n if (!isSubSet(parameters, validParameters)) {\r\n throw new Error('\"' + hook + '\" valid parameters are ' + JSON.stringify(validParameters));\r\n }\r\n if (!registry[hook]) {\r\n registry[hook] = [];\r\n }\r\n registry[hook].push([fn, parameters]);\r\n }\r\n\r\n function call(hook, args) {\r\n args = args || {};\r\n if (!definitions[hook]) {\r\n throw new Error('\"' + hook + '\" hook has not been defined');\r\n }\r\n if (!isSubSet(Object.keys(args), definitions[hook])) {\r\n throw new Error('\"' + hook + '\" valid arguments are ' + JSON.stringify(definitions[hook]));\r\n }\r\n \r\n var implementations = registry[hook] || [];\r\n var results = [];\r\n implementations.forEach(([fn, parameters]) => {\r\n /* Call with the correct arguments */\r\n var callWith = parameters.map(parameter => args[parameter]);\r\n var result = fn.apply(fn, callWith);\r\n if (result) {\r\n results.push(result);\r\n }\r\n });\r\n return results;\r\n }\r\n return {\r\n define: define,\r\n register: register,\r\n _registry: registry,\r\n call: call\r\n };\r\n})();\r\n```\r\nUsage:\r\n```javascript\r\ndatasette.plugins.define('numbers', ['a', 'b'])\r\ndatasette.plugins.register('numbers', (a, b) => a + b)\r\ndatasette.plugins.register('numbers', (a, b) => a * b)\r\ndatasette.plugins.call('numbers', {a: 4, b: 6})\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712260429, "label": "JavaScript plugin hooks mechanism similar to pluggy"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-752747999", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752747999, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc0Nzk5OQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T20:27:00Z", "updated_at": "2020-12-30T20:27:00Z", "author_association": "OWNER", "body": "I need to decide how this code is going to be loaded. Putting it in a blocking `