Skip to content

Commit

Permalink
support for ElementInternals and observedAttributes
Browse files Browse the repository at this point in the history
  • Loading branch information
cblanquera committed Sep 11, 2024
1 parent 0f1d719 commit 24acdff
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 41 deletions.
2 changes: 2 additions & 0 deletions packages/temple/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -32,6 +33,7 @@ export {
signal,
emitter,
TempleDataMap,
TempleField,
TempleComponent,
TempleRegistry,
TempleElement,
Expand Down
33 changes: 32 additions & 1 deletion packages/temple/src/client/TempleComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
*/
Expand Down Expand Up @@ -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;
Expand Down
71 changes: 46 additions & 25 deletions packages/temple/src/client/TempleEmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export default (() => {
//ex. <div mount=callback>Hello World</div>
bindAttribute('mount', element => {
const callback = element.getAttribute('mount');
if (typeof callback === 'function' ) {
if (typeof callback === 'function') {
const event = new CustomEvent('mount', {
detail: {
node: element,
Expand All @@ -201,7 +201,7 @@ export default (() => {
//ex. <div unmount=callback>Hello World</div>
unbindAttribute('unmount', element => {
const callback = element.getAttribute('unmount');
if (typeof callback === 'function' ) {
if (typeof callback === 'function') {
const event = new CustomEvent('unmount', {
detail: {
node: element,
Expand All @@ -215,42 +215,63 @@ export default (() => {
//ex. <div connect=callback>Hello World</div>
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. <div disconnect=callback>Hello World</div>
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. <div adopt=callback>Hello World</div>
bindAttribute('adopt', element => {
const callback = element.getAttribute('adopt');
if (typeof callback === 'function') {
emitter.unbind('adopt', callback);
emitter.on('adopt', callback);
}
});

//ex. <div associate=callback>Hello World</div>
bindAttribute('associate', element => {
const callback = element.getAttribute('associate');
if (typeof callback === 'function') {
emitter.unbind('associate', callback);
emitter.on('associate', callback);
}
});

//ex. <div disable=callback>Hello World</div>
bindAttribute('disable', element => {
const callback = element.getAttribute('disable');
if (typeof callback === 'function') {
emitter.unbind('disable', callback);
emitter.on('disable', callback);
}
});

//ex. <div reset=callback>Hello World</div>
bindAttribute('reset', element => {
const callback = element.getAttribute('reset');
if (typeof callback === 'function') {
emitter.unbind('reset', callback);
emitter.on('reset', callback);
}
});

//ex. <div attr=callback>Hello World</div>
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);
}
});

Expand Down
80 changes: 80 additions & 0 deletions packages/temple/src/client/TempleField.ts
Original file line number Diff line number Diff line change
@@ -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 <fieldset> 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;
// }
2 changes: 1 addition & 1 deletion packages/temple/src/client/classnames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'] || '';
}
34 changes: 34 additions & 0 deletions packages/temple/src/compiler/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -203,6 +214,29 @@ export default class Component {
return this._loader;
}

/**
* Returns attributes that should be observed
*/
public get observe() {
const observables = new Set<string>();
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
*/
Expand Down
27 changes: 22 additions & 5 deletions packages/temple/src/compiler/Transpiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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'];
Expand All @@ -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',
Expand Down
26 changes: 18 additions & 8 deletions packages/temple/src/document/Transpiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
Loading

0 comments on commit 24acdff

Please sign in to comment.