Where communities thrive


  • Join over 1.5M+ people
  • Join over 100K+ communities
  • Free without limits
  • Create your own community
People
Repo info
Activity
  • Jan 23 14:25
    smalluban labeled #89
  • Jan 23 14:11
    smalluban labeled #89
  • Jan 23 14:10
    darrickyee opened #89
  • Jan 21 08:50
    smalluban synchronize #85
  • Jan 21 08:22
    smalluban synchronize #85
  • Jan 20 13:22
    smalluban closed #30
  • Jan 20 10:07
    smalluban synchronize #85
  • Jan 17 13:57
    smalluban closed #88
  • Jan 17 13:51
    smalluban closed #86
  • Jan 17 13:51
    smalluban closed #87
  • Jan 17 13:31
    smalluban synchronize #87
  • Jan 17 10:05
    smalluban labeled #88
  • Jan 16 15:43
    scflode opened #88
  • Jan 15 18:46
    smalluban synchronize #87
  • Jan 15 18:32
    smalluban assigned #87
  • Jan 15 18:32
    smalluban edited #87
  • Jan 15 18:31
    smalluban opened #87
  • Jan 15 11:04
    smalluban assigned #85
  • Jan 15 07:07
    smalluban labeled #86
  • Jan 15 06:04
    auzmartist edited #86
Dominik Lubański
@smalluban

@riccardoscalco I looked at your <ternary-plot> source code, and I can't find defition of the data property. Hybrids can only "watch" properties, which are in the definition. If it is an object value, you should add to your index.js component definition the data property:

import { property } from 'hybrids';

export const TernaryPlot = {
   ...,
   data: property({}),
 };

This also answers the question from @Corvince. The property definition value is a default value. For example, if you want to have property name with "value" as default value, just define it like this:

const MyElement = {
  name: "value",
};

It uses under the hood translation concept (it translate it to property("value")).

Dominik Lubański
@smalluban
@riccardoscalco Your example works only because you set data before the first paint, so data object is available there. Try to not set data - then your component throws an error. Try to secure the case where element is used without passing data. As I wrote before, if you consume a property in the render for most cases it should be in the definition. I think the mistake come from the implicit usage of data property by the nested templates :)
Riccardo Scalco
@riccardoscalco
@smalluban Wonderful, thanks for the suggestion. I updated the module and the stackblitz demo.
Dominik Lubański
@smalluban
@jussiarpalahti it was a design decision not to use attributeChangedCallback. Take into account that many built-in elements do not update on attribute change, for example, input element. HTML attributes are a better option when you declaratively create UI with static values. Properties on the DOM are a better way to make dynamic changes. It might be possible to use other translation tool from your server, which uses properties instead of attributes (even though the HTML input have attributes).
Corvince
@Corvince

@smalluban thanks for the quick reply! And sorry I seem to have put my question poorly. What I want to do is to trigger a function with some side effects once a value changes. So for example

const MyCounter = {
   count: {
    observe: () => doSideEffects()
  }

Which sure enough works for changes in count, but the initial value of count will be "undefined" and so I can't do something like count += 1. Looking more into this, I think I can create a factory function that includes the default definitions for get (with a defaultvalue) and set. However, is there any way to initialize the value without calling the observe function?

Dominik Lubański
@smalluban
Look at the example in the factory section of the documentation. You don't have to use observe to react on changes - you can use get/set method to update something outside of your element, for example, document.title. However, if you want to have count property also as a attribute, you can define two properties, one with value and second with get and observe methods
This message was deleted
This message was deleted
Agh.. I am trying to write code snippet on mobile, and gitter still sends it with every return key. I will create a code example when I'll get home.
Corvince
@Corvince
Ah yes, doing it in the set method would be an option. The idea of using observe was to somewhat separate my pure functions from side effects. I think I'll stick with that for now and use a factory function for the default value and handle the initial call to the side effects function
Anyway thanks again for your quick answers!
Jussi Arpalahti
@jussiarpalahti
@smalluban thank you for your response
In my use case server has the state and browser only renders it
Custom elements are great fit for complex interaction handling, but I do need them to reflect server state changes. Morphdom is relatively lightweight option for this. Way I see this browser defers to server on this and all interactions get sent to server for evaluation. Thus each change produces new UI state from server rendered html.
Phoenix liveview uses additional change tracking and diffing functionality to get faster, lighter updates. I myself am just experimenting at this point and looked at Hybrids as a way to get functional custom elements from straightforward api. For my particular use case setAttribute is a requirement, but I'll keep it in mind. Especially the property pattern feels powerful tool I can see myself using.
Re: input values. I didn't know this. Interestingly firefox updates input value from setAttribute if element's value property hasn't been changed. If it has, attribute change does not modify property. Not at all what I was expecting. Morphdom seems to special case inputs and sets them through value property probably because of this very reason.
Dominik Lubański
@smalluban
@Corvince I meant something like this:
const MyElement = {
  count: 0,
  observedCount: {
    get: ({ count }) => count, // Now "count" property is a dependency of "observedCount"
    observe: (host, value, lastValue) => {
      // do side effect when count changes
    },
  },
};
Dominik Lubański
@smalluban
@jussiarpalahti I understand your case. Server produces html code with attributes, so it is natural to update those, not the properties. Also, properties not always reflect name of the property. In built-in property factory camelCase property nam is reflected with dashcase equivalent. However, the descriptor concept is very powerful and you can create your custom factory, which observes attribute name, and updates value. The only requirement is to define translation between property and attribute name (the latter is not case-sensitive and has more restriction to the chars used).
It could be something like this:
import { property } from 'hybrids';

export function propertyWithAttr(defaultValue) {
  return property(defaultValue, (host, key) => {
    const observer = new MutationObserver(() => { host[key] = host.getAttribute(keyOrSomething); });
    observer.observe(host), { attributes: true });
    return () => observer.disconnect();
  });
}
Dominik Lubański
@smalluban
Then you can use your factory:
const MyElement = {
  count: propertyWithAttr(0),
  ...
}
Jussi Arpalahti
@jussiarpalahti
@smalluban thank you for the example. I’ll have to play with this a bit. It looks like it should work. Through this Hybrids seems very amenable to augmentation which is great.
Corvince
@Corvince

@Corvince I meant something like this:

const MyElement = {
  count: 0,
  observedCount: {
    get: ({ count }) => count, // Now "count" property is a dependency of "observedCount"
    observe: (host, value, lastValue) => {
      // do side effect when count changes
    },
  },
};

What would be the advantage of this approach? Compared to using a single property:

const MyElement = {
  count: {
    get: (host, value = 0) => value, 
    observe: (host, value, lastValue) => {
      // do side effect when count changes
    },
  },
};
Dominik Lubański
@smalluban
There is a difference, but it might not be important for you. The first count property is translated to property(0), which means it supports attribute fallback and transformation to number. The latter is only a property with value, and in your example is read-only by the way. I suppose that I wasn't your intention. You have to create set manually, if you have get method. Even though you would have set method, it does not transform value to a number, nor uses attribute from html definition. It is fine if you are going to use your element within another built by the library. Built-in template engine uses properties at first, so passing value to nested elements in the template will work.
@jussiarpalahti About my code example - it might not be efficient as it should be. The mutation observer will trigger callback for any attribute change in the element. I suppose it should check mutations if there is a change in attribute, which we should watch. Also, it is for now one-way, it does not reflect property value back to attribute, but in your case it should not be a problem.
Corvince
@Corvince

I think I am slowly getting there. Thanks for taking your time. Finally, would this factory function provide the same thing, but within a single variable?

function observer(defaultValue = 0, func = () => sideEffects()) {
  const prop = property(defaultValue)
  prop.observe = func
  return prop
}

Just asking for clarification if there is any more "magic" happening in the translation process. and re: doing side-effects in the set-function: What's the (foreseen) difference between doing them in the set function and the observe function. I am guessing inside "set" it is always called and inside observe only if the value actually changed?

Dominik Lubański
@smalluban
Yes, observe is called async in requestAnimationFrame (so after macro and micro tasks, just before re-paint) and only if value of the property really has changed. As opposite, set method is called every time when you assert value to the property (by = notation), so it is called even value does not have to change. In your last example, you can use spread operator for simpler structure (the effect is similar):
return {
   ...property(defaultValue),
   observe: func,
};
Observe is quite new, property factory support passing connect method as a second argument, but it does not support passing observe. I think the observe method will not be commonly used with property factory, so I am not sure about adding it as next argument. After all, you can use above pattern to pass additional observe.
Corvince
@Corvince
Ah, nice use of the spread operator. Might then not even need a factory function, but just use your code snippet inside the Element definition
I think having connect as an optional argument, but not observe could be kind of confusing. Given this simple code structure, maybe you could even get rid of the connect argument? (Depending on if there will be even more properties)
const MyElement = {
  count: {
    ...property(0),
    connect: cfunc,
    observe: ofunc,
    futureProp: ffunc,
  }
}
Dominik Lubański
@smalluban
Yep, factory is just a function - it helps if you re-use the same or similar structure of the definition. About the property factory - I want to avoid making breaking changes as much as possible (even though we already have v4 :P), so the second argument of the property factory has to be a connect method, the only way to extend it is to add third argument - but then, if you want to use observe, you would have to pass second argument, even you don't need it. Because of that, I think it is not worth it - it may introduce more confusion than it would help.
Corvince
@Corvince
Nah, no reason to remove the connect argument soon. I was just thinking that maybe as the number properties could increase in the future and for some reason everyone will start to use futureProp it will at some point be strange that you can pass a custom connect function (which hypothetical nobody uses), but not the futureProp (which hypothetically everybody wants to use). So just saying that you could depreciate the feature and promote the alternative way in the docs, if that is at all a direction you want to go with hybrids. But there are probably more important things to consider right now ;)
Speaking of which: How is that built-in store going? Are you still working on this?
Georges Gomes
@georges-gomes
Hi Hybriders, we just opened https://webcomponents.dev with support and examples for Hybrids.
image.png
Looking forward for your feedback and if you want to add some nice components I will be happy to feature them on the site ;)
webcomponents with :heart:
Dominik Lubański
@smalluban
@georges-gomes I will definitely look at this. You have github link, so I will create an issue in the repo, if I find something to share with you.
Georges Gomes
@georges-gomes
@smalluban :pray: A blank sheet just for you :) https://github.com/webcomponents-dev/webcomponents.dev/issues
Dominik Lubański
@smalluban
@Corvince Sorry for the late response (I'm quite busy now on my daily paid work) - here you have a sneak peek of store feature - https://github.com/hybridsjs/hybrids/blob/store/test/spec/store.js. It is now on a separate branch, and not yet finished. Any questions and ideas welcome. In short - it will provide synchronous storage, which is connected to hybrids cache, but it is not a property factory. Still, you can safely use it inside of the property definitions and all will work as it should. It also provides the adapter idea - it allows to observe when the component wants something and in sync or async update the storage with the data. Because of that, the component will not have to worry about imperative call to async API and then render things. What yet left to be implemented is defining "many" relation to other models and how we can detect in "render" that something is being fetched :)
Fabien Bourgeois
@Yakulu
@smalluban Hi Dominik. I hope you're fine. Could you explain how the cache system works ? Are all states cached ? If yes, could we expect important memory usage in your experience ? Another point : do resolvers always work on the whole elements ? If an element is append to an array, does the whole array be evaluated to check if there are changes ? Thanks :)
Dominik Lubański
@smalluban

@Yakulu Thanks, I'm fine, quite busy at work, but trying to find some time to finish store feature ;) The cache mechanism is not as complicated as it might look. Every property is wrapped with the cache, so when you get or set property value cache controls what is returned (get) or updated (set). Because every property is controlled by the cache, it can store information about dependencies. If your property getter calls another property (even from another element, but created with the hybirds) it is saved as a dependency of that property. Then, If you observe changes of some property (by observe() method - this is how the render factory works) it is called when one of the dependency changes (by setting value or by calling invalidate, which then return new value). The dependency might be deep, so you can have chain of deps, like this render -> fullName -> firstName - if firstName is updated, the cache will add observe() method of the render property to update queue (using RAF).

To let it work the cache needs to store current and the last value of the property, so in case of memory usage, it has some cost, more significantly if your properties hold a big amount of data. However, the cache uses WeakMapto control references. They are tight to host element, so if you remove it from the DOM (and lose all references) the memory can be cleaned.

Another important fact is how the cache knows that something changed. It is done like other common solutions, eg. useMemo or PureComponent from React. It uses an equality check, so only the reference of the property value is used in the condition. It means that modifying the array will not update the property value. I am not sure what you meant with appending element to the array - but children factory replaces array each time when MutationObserver detects changes in the light DOM of the element - so when is a change in the DOM, the property will be updated with a new list of children.

Oh, and the last thing - the library controls only properties defined as a descriptor. It's obvious that cache does not control built-in element properties, like innerHTML, title or innerHeight that are come from HTMLElement prototype.
Fabien Bourgeois
@Yakulu
Thanks for your explanations. It makes sense. So Hybrids could be seen as reactive fine grained framework, like Solid or Sinuous ? About appending to an array, I was thinking about an array into the element host (state), rendering to a table for example. When you update the host through host.items = [...host.items, {a: 12}], is every item evaluated into the cache to check if they have changed ?
Dominik Lubański
@smalluban
I looked at Solid and Sinuous and there some similarities, but the main concept is different. In hybrids, you declare properties and how to generate them, and that's all - you don't have to worry about some useEffect() concept, or when to call your external API. In function based mode, the render function is called every time when props have changed. I know that Solid promises to call updates only when needed, and Sinuous has observable() pattern, so you can listen for changes (that is similar concept to observe() method in hybrids descriptor definition).
Look at the async example from the docs. The data is defined just as what it needs and you don't have to worry when it it will be called - this is where cache mechanism takes over the control.
data: ({ userId }) => getUser(userId),
And about rendering an array into the table. The cache control ends with calling render method (if you use render factory) if one of the render property dependencies have changed. From that point how rendering the DOM is made efficient is related to template engine. If you use built-in template engine from the hybrids, it also caches last values passed to the template. In case of the array, it uses index as default or keys defined by the user (with .key() helper). If you append new element into the array, template engine will reuse all of the values and append new one at the end of the table.
Fabien Bourgeois
@Yakulu
Solid is very well engineered but somewhat complicated.... Sinuous took parts of Solid and other libraries to push observable pattern indeed. Hybrids has a cleaner approach IMHO, more understandable for the developer. And some cool functional concepts (factories, translations etc).
OK for the array and keys usage, that's what I thought. Thanks again !
Dominik Lubański
@smalluban
No problem :) it's great for me to have hybrids users out there asking for help :)
Fabien Bourgeois
@Yakulu
Even if there had been major versions since, is your article Say goodbye to lifecycle methods, and focus on productive code still relevant to understand mechanisms ?
Fabien Bourgeois
@Yakulu
BTW hadn't seen article from June. Interesting moves, thanks.
Dominik Lubański
@smalluban
Yes, I updated article to changes from v4
Dominik Lubański
@smalluban
I didn't test it, but the latest release should also boost performance, as state recalculation of cache dependencies is much simpler now. It was using recursive call, now is just reducing one dimensional array of numbers.
Dominik Lubański
@smalluban
@/all New article from the core concepts series landed - Three unique features of the hybrids template engine that you must know - have a nice reading ;)
Fabien Bourgeois
@Yakulu
@smalluban I've not replied yet but thanks for this article, very useful !
Dominik Lubański
@smalluban
🤩