` such as `{{if thisIsTrue}}
stuff
{{/if}}` without restarting the server
+
+## Error when attempting to use local model paths in singleton components
+
+A [singleton component](../components/lifecycle#singleton-stateless-components) does not have a local model, so trying to use a local model path like `{{value}}` in its view will fail with this error:
+
+```
+TypeError: Cannot read properties of undefined (reading 'data')
+ at PathExpression.get
+ ...
+```
+
+To resolve the issue, bind the data via an attribute and refer to it with an attribute path `{{@value}}`. See the linked singleton component documentation for an example.
+
+Alternatively, if you don't need component controller functions, switch to using a plain [view partial](../components/view-partials) instead.
+
+## Mutation on uncreated remote document
+
+To perform mutations on a DB-backed document, it must first be loaded in the model. If not, an error `Error: Mutation on uncreated remote document` will be thrown.
+
+There are a few ways to load a document into the model:
+- [Fetching or subscribing to the document](../models/backends#loading-data-into-a-model), either directly via doc id or via a query
+- Creating a new document, e.g. via `model.add()`
+
+When a document is loaded with a [projection](https://share.github.io/sharedb/projections), the mutation must be done using the same projection name.
+- For example, if a doc was loaded only with a projection name `model.fetch('notes_title.note-12')`, then mutations must be done with the projection name, `model.set('notes_title.note-12.title', 'Intro')`.
+- Trying to mutate using the base collection name in that case, `model.set('notes.note-12.title')`, will result in the "Mutation on uncreated remote document" error.
+- If a doc is loaded both with the base collection name and with projections, then mutations can be done with any collection or projection name the doc was loaded with.
+
+## Invalid op submitted. Operation invalid in projected collection
+
+Make sure the field being mutated is one of the fields defined in the [projection](https://share.github.io/sharedb/projections).
+
+If that's not feasible, then fetch/subscribe the doc using its base collection name and do the mutation using the base collection.
diff --git a/src/components.ts b/src/components.ts
index 1b4c7970..2abb46c4 100644
--- a/src/components.ts
+++ b/src/components.ts
@@ -475,7 +475,19 @@ export class ComponentFactory {
// play nice with how CoffeeScript extends class constructors
emitInitHooks(context, component);
component.emit('init', component);
- if (component.init) component.init(component.model);
+ if (component.init) {
+ if (util.isProduction) {
+ component.init(component.model);
+ } else {
+ const initReturn: unknown = component.init(component.model);
+ if (initReturn instanceof Promise) {
+ console.warn(
+ `Component ${component.constructor.name} init() should not be an async function:`,
+ component.init
+ );
+ }
+ }
+ }
return component.context;
}
@@ -492,7 +504,7 @@ export class ComponentFactory {
function noop() {}
-class SingletonComponentFactory{
+class SingletonComponentFactory {
constructorFn: SingletonComponentConstructor;
isSingleton: true;
component: Component;
@@ -569,8 +581,18 @@ const _extendComponent = (Object.setPrototypeOf && Object.getPrototypeOf) ?
};
export function extendComponent(constructor: SingletonComponentConstructor | ComponentConstructor) {
- // Don't do anything if the constructor already extends Component
- if (constructor.prototype instanceof Component) return;
- // Otherwise, append Component.prototype to constructor's prototype chain
+ if (constructor.singleton) {
+ if (constructor.prototype instanceof Component) {
+ throw new Error('Singleton compoment must not extend the Component class');
+ } else {
+ return;
+ }
+ }
+ // Normal components' constructors must extend Component.
+ if (constructor.prototype instanceof Component) {
+ return;
+ }
+ // For backwards compatibility, if a normal component doesn't already extend Component,
+ // then append Component.prototype to the constructor's prototype chain
_extendComponent(constructor);
}
diff --git a/test/dom/components.mocha.js b/test/dom/components.mocha.js
index 746553ed..1d9adcca 100644
--- a/test/dom/components.mocha.js
+++ b/test/dom/components.mocha.js
@@ -1,5 +1,6 @@
var expect = require('chai').expect;
var pathLib = require('node:path');
+const { Component } = require('../../src/components');
var domTestRunner = require('../../src/test-utils/domTestRunner');
describe('components', function() {
@@ -54,5 +55,67 @@ describe('components', function() {
});
});
});
+
+ it('throws error if registering a singleton component that extends Component', () => {
+ const harness = runner.createHarness();
+
+ class MySingletonComponent extends Component {
+ }
+ MySingletonComponent.view = {
+ is: 'my-singleton-component',
+ source: '
My singleton
'
+ };
+ MySingletonComponent.singleton = true;
+ expect(() => {
+ harness.app.component(MySingletonComponent);
+ }).to.throw(Error, 'Singleton compoment must not extend the Component class');
+ });
+ });
+
+ describe('singleton components', () => {
+ it('do not have their own model when rendered', () => {
+ const harness = runner.createHarness();
+
+ class MySingletonComponent {
+ }
+ MySingletonComponent.view = {
+ is: 'my-singleton-component',
+ source: '{{@greeting}}
'
+ };
+ MySingletonComponent.singleton = true;
+ harness.app.component(MySingletonComponent);
+
+ harness.setup('');
+ harness.model.set('_page.greeting', 'Hello');
+
+ const renderResult = harness.renderHtml();
+ expect(renderResult.html).to.equal('Hello
');
+ // No Component instance created for singleton components
+ expect(renderResult.component).to.equal(undefined);
+ // Singleton components don't get a model allocated under '$components.' like
+ // normal components would.
+ expect(harness.model.get('$components')).to.equal(undefined);
+ });
+
+ it('can call view functions defined on the component class', () => {
+ const harness = runner.createHarness();
+
+ class MySingletonComponent {
+ emphasize(text) {
+ return text ? text.toUpperCase() : '';
+ }
+ }
+ MySingletonComponent.view = {
+ is: 'my-singleton-component',
+ source: '{{emphasize(@greeting)}}
'
+ };
+ MySingletonComponent.singleton = true;
+ harness.app.component(MySingletonComponent);
+
+ harness.setup('');
+
+ const renderResult = harness.renderHtml();
+ expect(renderResult.html).to.equal('HELLO
');
+ });
});
});