diff --git a/packages/temple/src/client.ts b/packages/temple/src/client.ts
index 167a77e..c06fd7f 100644
--- a/packages/temple/src/client.ts
+++ b/packages/temple/src/client.ts
@@ -7,6 +7,7 @@ export type {
} from './types';
import TempleException from './Exception';
+import TempleField from './client/TempleField';
import TempleComponent from './client/TempleComponent';
import TempleRegistry from './client/TempleRegistry';
import TempleElement from './client/TempleElement';
@@ -32,6 +33,7 @@ export {
signal,
emitter,
TempleDataMap,
+ TempleField,
TempleComponent,
TempleRegistry,
TempleElement,
diff --git a/packages/temple/src/client/TempleComponent.ts b/packages/temple/src/client/TempleComponent.ts
index a6a55e6..aae68e6 100644
--- a/packages/temple/src/client/TempleComponent.ts
+++ b/packages/temple/src/client/TempleComponent.ts
@@ -143,6 +143,37 @@ export default abstract class TempleComponent extends HTMLElement {
emitter.emit('adopt', this);
}
+ /**
+ * Called when static observedAttributes is set and:
+ * 1. setAttribute is called
+ * 2. removeAttribute is called
+ * 3. element.[attribute] is changed
+ * 4. element.[attribute] is removed
+ * 5. element.attributes is changed
+ * 6. Attribute is changed via browser developer tools
+ */
+ public attributeChangedCallback(
+ name: string,
+ prev: string|null,
+ next: string|null
+ ) {
+ //if it's rendering, do nothing
+ if (this._rendering) {
+ return;
+ }
+ //determine action
+ const action = prev === null
+ ? 'add' : next === null
+ ? 'remove' : 'update';
+ if (next === null && this.hasAttribute(name)) {
+ this.element.removeAttribute(name);
+ } else {
+ this.element.setAttribute(name, next);
+ }
+ //emit the attr event
+ emitter.emit('attr', { action, name, prev, value: next, target: this });
+ }
+
/**
* Called when the element is inserted into a document,
*/
@@ -289,7 +320,7 @@ export default abstract class TempleComponent extends HTMLElement {
} else {
//if shadow root is not set, create it
if (!this.shadowRoot) {
- this.attachShadow({ mode: 'open' });
+ this.attachShadow({ mode: 'open', delegatesFocus: true });
}
const shadowRoot = this.shadowRoot as ShadowRoot;
diff --git a/packages/temple/src/client/TempleEmitter.ts b/packages/temple/src/client/TempleEmitter.ts
index 10587d1..a742d7a 100644
--- a/packages/temple/src/client/TempleEmitter.ts
+++ b/packages/temple/src/client/TempleEmitter.ts
@@ -187,7 +187,7 @@ export default (() => {
//ex.
Hello World
bindAttribute('mount', element => {
const callback = element.getAttribute('mount');
- if (typeof callback === 'function' ) {
+ if (typeof callback === 'function') {
const event = new CustomEvent('mount', {
detail: {
node: element,
@@ -201,7 +201,7 @@ export default (() => {
//ex. Hello World
unbindAttribute('unmount', element => {
const callback = element.getAttribute('unmount');
- if (typeof callback === 'function' ) {
+ if (typeof callback === 'function') {
const event = new CustomEvent('unmount', {
detail: {
node: element,
@@ -215,42 +215,63 @@ export default (() => {
//ex. Hello World
bindAttribute('connect', element => {
const callback = element.getAttribute('connect');
- if (typeof callback === 'function' ) {
- const event = new CustomEvent('connect', {
- detail: {
- node: element,
- target: element.element
- }
- });
- callback(event);
+ if (typeof callback === 'function') {
+ emitter.unbind('connect', callback);
+ emitter.on('connect', callback);
}
});
//ex. Hello World
bindAttribute('disconnect', element => {
const callback = element.getAttribute('disconnect');
- if (typeof callback === 'function' ) {
- const event = new CustomEvent('disconnect', {
- detail: {
- node: element,
- target: element.element
- }
- });
- callback(event);
+ if (typeof callback === 'function') {
+ emitter.unbind('disconnect', callback);
+ emitter.on('disconnect', callback);
}
});
//ex. Hello World
bindAttribute('adopt', element => {
const callback = element.getAttribute('adopt');
+ if (typeof callback === 'function') {
+ emitter.unbind('adopt', callback);
+ emitter.on('adopt', callback);
+ }
+ });
+
+ //ex. Hello World
+ bindAttribute('associate', element => {
+ const callback = element.getAttribute('associate');
+ if (typeof callback === 'function') {
+ emitter.unbind('associate', callback);
+ emitter.on('associate', callback);
+ }
+ });
+
+ //ex. Hello World
+ bindAttribute('disable', element => {
+ const callback = element.getAttribute('disable');
+ if (typeof callback === 'function') {
+ emitter.unbind('disable', callback);
+ emitter.on('disable', callback);
+ }
+ });
+
+ //ex. Hello World
+ bindAttribute('reset', element => {
+ const callback = element.getAttribute('reset');
+ if (typeof callback === 'function') {
+ emitter.unbind('reset', callback);
+ emitter.on('reset', callback);
+ }
+ });
+
+ //ex. Hello World
+ bindAttribute('attr', element => {
+ const callback = element.getAttribute('attr');
if (typeof callback === 'function' ) {
- const event = new CustomEvent('adopt', {
- detail: {
- node: element,
- target: element.element
- }
- });
- callback(event);
+ emitter.unbind('attr', callback);
+ emitter.on('attr', callback);
}
});
diff --git a/packages/temple/src/client/TempleField.ts b/packages/temple/src/client/TempleField.ts
new file mode 100644
index 0000000..f92c9a3
--- /dev/null
+++ b/packages/temple/src/client/TempleField.ts
@@ -0,0 +1,80 @@
+import TempleComponent from './TempleComponent';
+import emitter from './TempleEmitter';
+
+export default abstract class TempleField extends TempleComponent {
+ //associates this component with a form
+ public static formAssociated = true;
+ //accessor to the form internals API
+ protected _field: ElementInternals;
+
+ /**
+ * Returns the form internals API.
+ */
+ public get field() {
+ return this._field;
+ }
+
+ /**
+ * Attaches the form internals API to the element.
+ */
+ public constructor() {
+ super();
+ this._field = this.attachInternals();
+ }
+
+ /**
+ * This is called as soon as the element is associated with a form.
+ */
+ public formAssociatedCallback(form: HTMLFormElement) {
+ //emit the associate event
+ emitter.emit('associate', this);
+ }
+
+ /**
+ * This is called whenever the element or parent element are disabled.
+ */
+ public formDisabledCallback(disabled: boolean) {
+ //emit the disable event
+ emitter.emit('disable', this);
+ }
+
+ /**
+ * This gives us the ability to control our element’s behavior when a user resets a form.
+ */
+ public formResetCallback() {
+ //emit the disable event
+ emitter.emit('reset', this);
+ }
+}
+
+// Just to note..
+// interface ValidityStateFlags {
+// // `true` if the element is required, but has no value
+// valueMissing?: boolean;
+// // `true` if the value is not in the required syntax
+// // (when the "type" is "email" or "URL")
+// typeMismatch?: boolean;
+// // `true` if the value does not match the specified pattern
+// patternMismatch?: boolean;
+// // `true` if the value exceeds the specified `maxlength`
+// tooLong?: boolean;
+// // `true` if the value fails to meet the specified `minlength`
+// tooShort?: boolean;
+// // `true` if the value is less than the
+// // minimum specified by the `min` attribute
+// rangeUnderflow?: boolean;
+// // `true` if the value is greater than the
+// // maximum specified by the `max` attribute
+// rangeOverflow?: boolean;
+// // `true` if the value does not fit the rules determined by the
+// // `step` attribute (that is, it's not evenly divisible by the
+// // step value)
+// stepMismatch?: boolean;
+// // `true` if the user has provided input
+// // that the browser is unable to convert
+// badInput?: boolean;
+// // `true` if the element's custom validity message has been set to
+// // a non-empty string by calling the element's `setCustomValidity()`
+// // method
+// customError?: boolean;
+// }
\ No newline at end of file
diff --git a/packages/temple/src/client/classnames.ts b/packages/temple/src/client/classnames.ts
index 702c76f..1875198 100644
--- a/packages/temple/src/client/classnames.ts
+++ b/packages/temple/src/client/classnames.ts
@@ -25,5 +25,5 @@ export function classlist(pointer: Component|null = null) {
* ie. const classes = classnames();
*/
export default function classnames(pointer: TempleComponent|null = null) {
- return props<{'class': string}>(pointer)['class'];
+ return props<{'class': string}>(pointer)['class'] || '';
}
\ No newline at end of file
diff --git a/packages/temple/src/compiler/Component.ts b/packages/temple/src/compiler/Component.ts
index e0c04ef..ceadc8a 100644
--- a/packages/temple/src/compiler/Component.ts
+++ b/packages/temple/src/compiler/Component.ts
@@ -160,6 +160,17 @@ export default class Component {
return path.dirname(this.absolute);
}
+ /**
+ * Returns true if the component should be treated as a field
+ */
+ public get field() {
+ return this.ast.scripts.some(
+ script => script.attributes?.properties.some(
+ property => property.key.name === 'form'
+ )
+ );
+ }
+
/**
* Returns the filesystem being used
*/
@@ -203,6 +214,29 @@ export default class Component {
return this._loader;
}
+ /**
+ * Returns attributes that should be observed
+ */
+ public get observe() {
+ const observables = new Set();
+ for (const script of this.ast.scripts) {
+ if (script.attributes) {
+ for (const property of script.attributes.properties) {
+ const { key, value } = property;
+ if (key.name === 'observe'
+ && value.type === 'Literal'
+ && typeof value.value === 'string'
+ ) {
+ value.value.split(',').forEach(
+ attribute => observables.add(attribute.trim())
+ );
+ }
+ }
+ }
+ }
+ return Array.from(observables);
+ }
+
/**
* Returns the parent component
*/
diff --git a/packages/temple/src/compiler/Transpiler.ts b/packages/temple/src/compiler/Transpiler.ts
index 9a27811..2c315ee 100644
--- a/packages/temple/src/compiler/Transpiler.ts
+++ b/packages/temple/src/compiler/Transpiler.ts
@@ -101,22 +101,31 @@ export default class Transpiler {
classname,
imports,
styles,
- scripts
+ scripts,
+ field,
+ observe
} = this._component;
//determine tagname
const tagname = this._component.brand
? `${this._component.brand}-${this._component.tagname}`
: this._component.tagname;
+ //determine component parent
+ const parent = field ? 'TempleField' : 'TempleComponent';
//get path without extension
//ex. /path/to/Counter.tml -> /path/to/Counter
const extname = path.extname(absolute);
const filePath = absolute.slice(0, -extname.length);
//create a new source file
const { source } = this._createSourceFile(`${filePath}.ts`);
- //import { TempleRegistry, TempleComponent } from '@ossph/temple/client';
+ //import TempleRegistry from '@ossph/temple/dist/client/TempleRegistry';
source.addImportDeclaration({
- moduleSpecifier: '@ossph/temple/client',
- namedImports: [ 'TempleRegistry', 'TempleComponent' ]
+ moduleSpecifier: '@ossph/temple/dist/client/TempleRegistry',
+ defaultImport: 'TempleRegistry'
+ });
+ //import TempleComponent from '@ossph/temple/dist/client/TempleComponent';
+ source.addImportDeclaration({
+ moduleSpecifier: `@ossph/temple/dist/client/${parent}`,
+ defaultImport: parent
});
//import Counter from './Counter'
this._component.components.filter(
@@ -156,7 +165,7 @@ export default class Transpiler {
//export default class FoobarComponent extends TempleComponent
const component = source.addClass({
name: classname,
- extends: 'TempleComponent',
+ extends: parent,
isDefaultExport: true,
});
//public static component = ['foo-bar', 'FoobarComponent'];
@@ -165,6 +174,14 @@ export default class Transpiler {
isStatic: true,
initializer: `[ '${tagname}', '${classname}' ] as [ string, string ]`
});
+ //public static observedAttributes = ["required", "value"]
+ if (observe.length > 0) {
+ component.addProperty({
+ name: 'observedAttributes',
+ isStatic: true,
+ initializer: JSON.stringify(observe)
+ });
+ }
//public style()
component.addMethod({
name: 'styles',
diff --git a/packages/temple/src/document/Transpiler.ts b/packages/temple/src/document/Transpiler.ts
index 3e35e41..9336048 100644
--- a/packages/temple/src/document/Transpiler.ts
+++ b/packages/temple/src/document/Transpiler.ts
@@ -121,15 +121,25 @@ export default class Transpiler extends ComponentTranspiler {
'Hash'
]
});
- //import { TempleRegistry, emitter, data as __APP_DATA__ } from '@ossph/temple/client';
+ //import TempleException from '@ossph/temple/dist/Exception';
source.addImportDeclaration({
- moduleSpecifier: '@ossph/temple/client',
- namedImports: [
- 'TempleRegistry',
- 'TempleException',
- 'emitter',
- 'data as __APP_DATA__'
- ]
+ moduleSpecifier: '@ossph/temple/dist/Exception',
+ defaultImport: 'TempleException'
+ });
+ //import TempleRegistry from '@ossph/temple/dist/client/TempleRegistry';
+ source.addImportDeclaration({
+ moduleSpecifier: '@ossph/temple/dist/client/TempleRegistry',
+ defaultImport: 'TempleRegistry'
+ });
+ //import emitter from '@ossph/temple/dist/client/TempleEmitter';
+ source.addImportDeclaration({
+ moduleSpecifier: '@ossph/temple/dist/client/TempleEmitter',
+ defaultImport: 'emitter'
+ });
+ //import __APP_DATA__ from '@ossph/temple/dist/client/data';
+ source.addImportDeclaration({
+ moduleSpecifier: '@ossph/temple/dist/client/data',
+ defaultImport: '__APP_DATA__'
});
//import Counter_abc123 from './Counter_abc123'
registry.forEach(component => {
diff --git a/packages/temple/tests/fixtures/page.dtml b/packages/temple/tests/fixtures/page.dtml
index 5791503..dc2615a 100644
--- a/packages/temple/tests/fixtures/page.dtml
+++ b/packages/temple/tests/fixtures/page.dtml
@@ -8,7 +8,7 @@
padding: 0;
}
-