Repository: ProjectEvergreen/wcc (js)

Issue:
provide component data through render method params (constructor props)
## Type of Change
Feature

## Summary
As part of https://github.com/ProjectEvergreen/greenwood/pull/1157, an alternative to `getData` was identified that could be more useful at the framework level as `getData` is more of a component / entry point level mechanic, but frameworks will likely have / want to implement their own data loading mechanics, so being able to DI data from the framework would be nice.  For example, Greenwood would like to be able to pass in an instance of a `Request` object, and possibly additional framework metadata.

On top of that frameworks, may want to define their own naming convention for data loading, the most common one being the `loader` pattern, e.g.
```js
export default class PostPage extends HTMLElement {
  constructor(request) {
    super();
    
    const params = new URLSearchParams(request.url.slice(request.url.indexOf('?')));
    this.postId = params.get('id');
  }

  async connectedCallback() {
    const { postId } = this;
    const post = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`).then(resp => resp.json());
    const { id, title, body } = post;

    this.innerHTML = `
      <h1>Fetched Post ID: ${id}</h1>
      <h2>${title}</h2>
      <p>${body}</p>
    `;
  }
}
```

## Details 
In theory a framework could import / call `getData` itself as a generic `loader` pattern, WCC would still run it itself too as part of rendering, incurring a double fetch.  Also this makes the name flexible, and could let frameworks take on more complex requirements like https://github.com/ProjectEvergreen/greenwood/issues/880.

So instead of relying only on WCC to do the data fetching and orchestration, we could pass these "constructor props" to WCC and it can inject that data into the top level component of the entry point, e.g.
```js
const request = new Request({ /* ... */ });
const { html } = await renderToString(new URL(moduleUrl), false, request);
```

----

This may beg the question if WCC should even have a data loading mechanic, so it might be good to spin up an issue or discussion for that.

Pull request description:
feature/issue 119 constructor props
<!--
## Submitting a Pull Request
We love contributions and appreciate any help you can offer!
-->

## Related Issue
resolves #119 

## Summary of Changes
1. Support constructor props as `renderToString` param
1. Add documentation and test case

## TODO
1. [x] Add test case
1. [x] Document constructor props
1. [x] Needs https://github.com/ProjectEvergreen/wcc/pull/121 if we want to use `fetch` in our specs
1. [x] Does this apply to `renderFromHTML`? (moved to discussion)
1. [x] proof read docs
1. [x] `getData` deprecation issue tracking - https://github.com/ProjectEvergreen/wcc/discussions/123

Patch:
diff --git a/docs/pages/docs.md b/docs/pages/docs.md
index 9277eb5..4e8c64d 100644
--- a/docs/pages/docs.md
+++ b/docs/pages/docs.md
@@ -144,10 +144,76 @@ The benefit is that this hint can be used to defer loading of these scripts by u
 
 ## Data
 
-To further support SSR and hydration scenarios where data is involved, a file with a custom element definition can also export a `getData` function to inject into the custom elements constructor at server render time, as "props".  This can be serialized right into the component's Shadow DOM!
+WCC provide a couple mechanisms for data loading.
 
-For example, you could preload a counter component with an initial counter state
+### Constructor Props
+
+Often for frameworks that might have their own needs for data loading and orchestration, a top level "constructor prop" can be provided to `renderToString` as the final param.  The prop will then be passed to the custom element's `constructor` when loading the module URL.
+
+<!-- eslint-disable no-unused-vars -->
+```js
+const request = new Request({ /* ... */ });
+const { html } = await renderToString(new URL(moduleUrl), false, request);
+```
+
+This pattern plays really nice with file-based routing and SSR!
+```js
+export default class PostPage extends HTMLElement {
+  constructor(request) {
+    super();
+
+    const params = new URLSearchParams(request.url.slice(request.url.indexOf('?')));
+    this.postId = params.get('id');
+  }
+
+  async connectedCallback() {
+    const { postId } = this;
+    const post = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`).then(resp => resp.json());
+    const { title, body } = post;
+
+    this.innerHTML = `
+      <h2>${title}</h2>
+      <p>${body}</p>
+    `;
+  }
+}
+```
+
+### Data Loader
+
+To support component-level data loading and hydration scenarios, a file with a custom element definition can also export a `getData` function to inject into the custom elements constructor at build time.  This can be serialized right into the component's Shadow DOM!
+
+For example, you could preload a counter component with an initial counter state, which would also come through the `constructor`.
+
+<!-- eslint-disable no-unused-vars -->
 ```js
+class Counter extends HTMLElement {
+  constructor(props = {}) {
+    super();
+
+    this.count = props.count;
+    this.render();
+  }
+
+  // ....
+
+  render() {
+    return `
+      <template shadowroot="open">
+        <script type="application/json">
+          ${JSON.stringify({ count: this.count })}
+        </script>
+
+        <div>
+          <button id="inc">Increment</button>
+          <span>Current Count: <span id="count">${this.count}</span></span>
+          <button id="dec">Decrement</button>
+        </div>
+      </template>
+    `;
+  }
+}
+
 export async function getData() {
   return {
     count: Math.floor(Math.random() * (100 - 0 + 1) + 0)
diff --git a/src/wcc.js b/src/wcc.js
index b27278f..f9b3998 100644
--- a/src/wcc.js
+++ b/src/wcc.js
@@ -126,7 +126,7 @@ async function getTagName(moduleURL) {
   return tagName;
 }
 
-async function initializeCustomElement(elementURL, tagName, attrs = [], definitions = [], isEntry) {
+async function initializeCustomElement(elementURL, tagName, attrs = [], definitions = [], isEntry, props = {}) {
   if (!tagName) {
     const depth = isEntry ? 1 : 0;
     registerDependencies(elementURL, definitions, depth);
@@ -138,7 +138,11 @@ async function initializeCustomElement(elementURL, tagName, attrs = [], definiti
     ? customElements.get(tagName)
     : (await import(pathname)).default;
   const dataLoader = (await import(pathname)).getData;
-  const data = dataLoader ? await dataLoader() : {};
+  const data = props
+    ? props
+    : dataLoader
+      ? await dataLoader(props)
+      : {};
 
   if (element) {
     const elementInstance = new element(data); // eslint-disable-line new-cap
@@ -160,11 +164,11 @@ async function initializeCustomElement(elementURL, tagName, attrs = [], definiti
   }
 }
 
-async function renderToString(elementURL, wrappingEntryTag = true) {
+async function renderToString(elementURL, wrappingEntryTag = true, props = {}) {
   const definitions = [];
   const elementTagName = wrappingEntryTag && await getTagName(elementURL);
   const isEntry = !!elementTagName;
-  const elementInstance = await initializeCustomElement(elementURL, undefined, undefined, definitions, isEntry);
+  const elementInstance = await initializeCustomElement(elementURL, undefined, undefined, definitions, isEntry, props);
 
   const elementHtml = elementInstance.shadowRoot
     ? elementInstance.getInnerHTML({ includeShadowRoots: true })


Test patch:
diff --git a/test/cases/constructor-props/constructor-props.spec.js b/test/cases/constructor-props/constructor-props.spec.js
new file mode 100644
index 0000000..816b986
--- /dev/null
+++ b/test/cases/constructor-props/constructor-props.spec.js
@@ -0,0 +1,43 @@
+/*
+ * Use Case
+ * Run wcc against a custom element passing in constructor props.
+ *
+ * User Result
+ * Should return the expected HTML output based on the fetched content from constructor props.
+ *
+ * User Workspace
+ * src/
+ *   index.js
+ */
+
+import chai from 'chai';
+import { JSDOM } from 'jsdom';
+import { renderToString } from '../../../src/wcc.js';
+
+const expect = chai.expect;
+
+describe('Run WCC For ', function() {
+  const LABEL = 'Custom Element w/ constructor props';
+  const postId = 1;
+  let dom;
+
+  before(async function() {
+    const { html } = await renderToString(new URL('./src/index.js', import.meta.url), false, postId);
+
+    dom = new JSDOM(html);
+  });
+
+  describe(LABEL, function() {
+    it('should have a heading tag with the postId', function() {
+      expect(dom.window.document.querySelectorAll('h1')[0].textContent).to.equal(`Fetched Post ID: ${postId}`);
+    });
+
+    it('should have a second heading tag with the title', function() {
+      expect(dom.window.document.querySelectorAll('h2')[0].textContent).to.equal('sunt aut facere repellat provident occaecati excepturi optio reprehenderit');
+    });
+
+    it('should have a heading tag with the body', function() {
+      expect(dom.window.document.querySelectorAll('p')[0].textContent.startsWith('quia et suscipit')).to.equal(true);
+    });
+  });
+});
\ No newline at end of file
diff --git a/test/cases/constructor-props/src/index.js b/test/cases/constructor-props/src/index.js
new file mode 100644
index 0000000..9f7bfd1
--- /dev/null
+++ b/test/cases/constructor-props/src/index.js
@@ -0,0 +1,19 @@
+export default class PostPage extends HTMLElement {
+  constructor(postId) {
+    super();
+
+    this.postId = postId;
+  }
+
+  async connectedCallback() {
+    const { postId } = this;
+    const post = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`).then(resp => resp.json());
+    const { id, title, body } = post;
+
+    this.innerHTML = `
+      <h1>Fetched Post ID: ${id}</h1>
+      <h2>${title}</h2>
+      <p>${body}</p>
+    `;
+  }
+}
\ No newline at end of file