Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 39 additions & 10 deletions dist/lego.js
Original file line number Diff line number Diff line change
Expand Up @@ -632,12 +632,17 @@ function render(vnode, parentDomNode, options = {}) {
}
}

const _toCamelCaseCache = {};
function toCamelCase(name) {
if (_toCamelCaseCache[name] !== undefined) {
return _toCamelCaseCache[name]
}
if(name.includes('-')) {
const parts = name.split('-');
name = parts[0] + parts.splice(1).map(s => s[0].toUpperCase() + s.substr(1)).join('');
_toCamelCaseCache[name] = parts[0] + parts.splice(1).map(s => s[0].toUpperCase() + s.substr(1)).join('');
return _toCamelCaseCache[name]
}
return name
return name;
}


Expand All @@ -647,10 +652,13 @@ class Component extends HTMLElement {
this.useShadowDOM = true;
this.__isConnected = false;
this.__state = {};
this.__stores = []; // keep track of store to unsubscribe automatically
if(this.init) this.init();
this.watchProps = Object.keys(this.__state);
this.__attributesToState();
this.document = this.useShadowDOM ? this.attachShadow({mode: 'open'}) : this;
this.__debounceRender = null;
if(this.afterInit) this.afterInit();
}

__attributesToState() {
Expand All @@ -664,7 +672,11 @@ class Component extends HTMLElement {
get vstyle() { return ({ state }) => '' }

setAttribute(name, value) {
super.setAttribute(name, value);
if (name.indexOf('-') === -1) {
// call default setAttribute only for standard HTML attributes
// For example, we can exchange big JSON string between components without printing it in the DOM
super.setAttribute(name, value);
}
const prop = toCamelCase(name);
if(this.watchProps.includes(prop)) this.render({ [prop]: value });
}
Expand All @@ -678,23 +690,30 @@ class Component extends HTMLElement {
}
}

debounce(func, timeout = 300, ...args){
clearTimeout(func.debounceTimer);
func.debounceTimer = setTimeout(() => { func.apply(this, args); }, timeout);
}

async connectedCallback() {
this.__isConnected = true;
await this.render();
this.render({}, false);
// First rendering of the component
if(this.connected) this.connected();
}

disconnectedCallback() {
this.__isConnected = false;
this.setState({});
this.__stores.forEach(store => store.unsubscribe(this));
if(this.disconnected) this.disconnected();
}

async setState(props = {}) {
Object.assign(this.__state, props);
if(this.changed && this.__isConnected) await this.changed(props);
}
setState(updated = {}) {
const previous = Object.keys(updated).reduce((obj, key) => Object.assign(obj, { [key]: this.__state[key] }), {});
Object.assign(this.__state, updated);
if(this.changed && this.__isConnected) this.changed(updated, previous);
}

set state(value) {
this.setState(value);
Expand All @@ -704,9 +723,19 @@ class Component extends HTMLElement {
return this.__state
}

async render(state) {
await this.setState(state);
render(state, debounce = true) {
this.setState(state);
if(!this.__isConnected) return
if (debounce === false) return this.renderDebounce();
clearTimeout(this.__debounceRender);
// debounce to avoid 3 call when the parent component pass 3 attributes to a child component.
// In that case, this.setAttribute is called 3 times by petit dom when re-creating the dom
this.__debounceRender = setTimeout(() => {
this.renderDebounce();
}, 0);
}

renderDebounce() {
return render([
this.vdom({ state: this.__state }),
this.vstyle({ state: this.__state }),
Expand Down
2 changes: 1 addition & 1 deletion dist/lego.min.js

Large diffs are not rendered by default.

Binary file modified dist/lego.min.js.gz
Binary file not shown.
54 changes: 43 additions & 11 deletions dist/store.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
class Store {
constructor(state = {}, actions = {}) {
constructor(state = {}) {
this.__state = state;
this.actions = actions;
this.subscribers = [];
}

subscribe(subscriber, props = []) {
const selectedProps = Array.isArray(props) ? props : Object.keys(props);
subscriber.setState(this.getSelectedState(selectedProps));
this.subscribers.push({ target: subscriber, props: selectedProps });
subscriber.__stores.push(this);
}

unsubscribe (subscriber) {
const activeSubscribers = [];
for (let i = 0; i < this.subscribers.length; i++) {
const sub = this.subscribers[i];
if (sub.target !== subscriber) {
activeSubscribers.push(sub);
}
}
this.subscribers = activeSubscribers;
}

get state() {
Expand All @@ -17,7 +28,7 @@ class Store {

setState(newState) {
this.__state = { ...this.state, ...newState };
this.notify();
this.notify(newState);
}

getSelectedState(selectedProps) {
Expand All @@ -29,17 +40,38 @@ class Store {
}, {})
}

notify() {
this.subscribers.forEach(({ target, props }) => {
target.render(this.getSelectedState(props));
});
updateSelectedState(updatedProps, subscribedProps) {
const _selectedState = {};
let _atLeastOneUpdate = 0;
for (var i = 0; i < subscribedProps.length; i++) {
const _prop = subscribedProps[i];
if (updatedProps === undefined || updatedProps[_prop] !== undefined) {
_selectedState[_prop] = this.state[_prop];
_atLeastOneUpdate |= 1;
}
}
if (_atLeastOneUpdate === 1) {
return _selectedState;
}
return null;
}

dispatch(actionName, ...payload) {
const action = this.actions[actionName];
if (action) action.bind(this)(...payload);
else throw new Error(`action "${actionName}" does not exist`)
/**
* Notify all subscribers
*
* @param {Object} updatedProps [Optional] The updated properties (= partial new state). Ex { att1 : 1, att2 : 2}
*/
notify(updatedProps) {
this.subscribers.forEach(({ target, props }) => {
const _stateUpdated = this.updateSelectedState(updatedProps, props);
if (_stateUpdated !== null) {
// update DOM only if the subscriber is listening the updated prop
// console.log('notify', target, props)
target.render(_stateUpdated);
}
});
}

}

export { Store as default };
2 changes: 1 addition & 1 deletion dist/store.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file modified dist/store.min.js.gz
Binary file not shown.
5 changes: 3 additions & 2 deletions docs/_v1/20.06-reactive.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@ function updateStatus(event) {

This will update your component only where needed.

When `state` is just mutated, the `changed(changedProps)` is called.
This `changed()` method is called before (re-)rendering.
When `state` is just mutated, the `changed(newValues, oldValues)` is called.
This `changed()` method is called before (re-)rendering and will provide you
with the 2 arguments to enable to compare changes and react accordingly.
15 changes: 12 additions & 3 deletions docs/_v2/20.05-event.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,23 @@ parent: Usage

#### `@` Directive for binding Events

The `@` directive will call a method declared in the `methods` const:

```html
<script>
function sayHi(event) {
const buttonName = event.target.name
alert(`You clicked ${ buttonName } to says hi! 👋🏼`)
const methods = {
sayHi(event) {
const buttonName = event.target.name
alert(`You clicked ${ buttonName } to says hi! 👋🏼`)
}
}
</script>
<template>
<button @click="sayHi" name="the-button">click</button>
</template>
```

Any method in that `methods` declaration can be invoked as a method of the component
with the context of the component itself. Meaning that `this` refers to the component.
These methods can therefore access `this.state` for the current state,
`this.render()` for re-rendering…
12 changes: 2 additions & 10 deletions docs/_v2/20.09-reactive-style.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,7 @@ Writing CSS is as easy as
</template>

<script>
export default class extends Lego {
init() {
this.state = { fontScale: 1 }
}
}
const state = { fontScale: 1 }
</script>

<style>
Expand Down Expand Up @@ -57,11 +53,7 @@ Example:
<h1>Bonjour<h1>
</template>
<script>
export default class extends Lego {
init() {
this.state = { color: '#357' }
}
}
const state = { color: '#357' }
</script>
<style>
h1 {
Expand Down
7 changes: 7 additions & 0 deletions docs/_v2/20.10-script.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ nav_order: 10
parent: Usage
---

## `<script>` tag

This tag holds JavaScript with imports, and any regular JS code.
It is fully scoped to the current component instance.

It has several reserved keywords: `state`, `methods`, `setup`, `connected` and `Lego`.
Refer to respective sections for further details about what these keywords do.


## `<script extend>` tag
Expand Down
6 changes: 4 additions & 2 deletions docs/_v2/60-advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ __bricks/user-profile.html__
fruits: [{ name: 'Apple', icon: '🍎' }, { name: 'Pineapple', icon: '🍍' }]
}

function register() {
render({ registered: confirm('You are about to register…') })
const methods = {
register() {
this.render({ registered: confirm('You are about to register…') })
}
}
</script>

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@polight/lego",
"version": "2.0.0-beta.6",
"name": "lego",
"version": "2.0.0-beta.1003",
"description": "Low-Tech & Future-Proof Web-Components library",
"main": "index.js",
"type": "module",
Expand Down
7 changes: 2 additions & 5 deletions src/compiler/transpiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ function parseHtmlComponent(html) {
const template = serialize(parseFragment(html).childNodes.find(n => n.tagName === 'template')?.content)
const style = parseFragment(html).childNodes.find(n => n.tagName === 'style')?.childNodes[0].value

// Make it compatible with previous full class versions
// Make it backward compatible with previous full class versions (Lego v1)
if(script && !extendScript && script.includes('export default class')) {
extendScript = script
script = ""
script = ''
}

return { script, extendScript, template, style }
Expand Down Expand Up @@ -48,9 +48,6 @@ function generateFileContent({ dom, config, version }) {
${ dom.extendScript ? '' : 'export default ' }class ${ config.baseClassName } extends Component {
init() {
this.useShadowDOM = ${ Boolean(config.useShadowDOM) }
if(typeof state === 'object') this.__state = Object.assign({}, state, this.__state)
if(typeof connected === 'function') this.connected = connected
if(typeof setup === 'function') setup.bind(this)()
}
get vdom() { return __template }
get vstyle() { return __style }
Expand Down
3 changes: 1 addition & 2 deletions src/compiler/vdom-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ function extractDirectives(node) {
}
else if(name.startsWith('@')) {
name = `on${name.slice(1)}`
// Complexity due to retro-compatibility (class version).
// Short will be: const func = `${value}.bind(this)`
const func = `(typeof ${value} === 'function' ? ${value}.bind(this) : this.${value}).bind(this)`
//TODO understand const func = `this.${value}.bind(this)`
attrs.push({ name, value: func })
}
else attrs.push({ name, value: `\`${value}\`` })
Expand Down
Loading