Where communities thrive


  • Join over 1.5M+ people
  • Join over 100K+ communities
  • Free without limits
  • Create your own community
People
Repo info
Activity
  • Sep 17 00:33
    waspeer closed #65
  • Sep 15 15:15
    smalluban labeled #65
  • Sep 15 15:15
    smalluban unlabeled #65
  • Sep 15 08:04
    smalluban labeled #65
  • Sep 15 08:04
    smalluban labeled #65
  • Sep 15 00:01
    waspeer opened #65
  • Sep 09 09:25
    smalluban closed #64
  • Sep 09 03:43
    nullset closed #63
  • Sep 09 03:07
    metasean opened #64
  • Sep 08 04:59
    nullset synchronize #63
  • Sep 07 03:48
    nullset synchronize #63
  • Sep 07 03:36
    nullset opened #63
  • Jul 30 05:05
    akoushke closed #62
  • Jul 27 10:01
    smalluban labeled #62
  • Jul 26 17:47
    akoushke edited #62
  • Jul 26 17:47
    akoushke edited #62
  • Jul 26 17:45
    akoushke edited #62
  • Jul 26 17:45
    akoushke opened #62
  • Jul 18 13:08
    smalluban closed #61
  • Jul 18 09:21
    smalluban closed #48
Benny Powers
@bennypowers

hey, I upgraded to v3.0.2, but now I'm getting test failures for:

    describe('when there is an observableQuery', function() {
      let el;
      let setVariablesSpy;
      const query = gql`query { foo }`;

      beforeEach(async function() {
        el = await getElement({ client, query });
        setVariablesSpy = stub(el.observableQuery, 'setVariables');
      });

      afterEach(function() {
        setVariablesSpy.restore();
      });

      it('calls observableQuery.subscribe', async function setVariablesCallsObservableQuerySetVariables() {
        // shouldn't this be an instance of ObservableQuery?
        el.variables = { errorPolicy: 'foo' };
        expect(setVariablesSpy).to.have.been.calledWith(match({ errorPolicy: 'foo' }));
      });
    });

code:

function subscribeOrSetVariables(host, variables) {
  return variables &&
      host.observableQuery &&
      host.observableQuery.setVariables &&
      typeof host.observableQuery.setVariables === 'function'
    ? host.observableQuery.setVariables(variables)
    : host.subscribe({ variables });
}

function canSubscribe(host, key, target) {
  return !!(host === target && host[key]);
}

const variables = {
  connect: (host, key) => {
    const onInvalidate = ({ target }) =>
      canSubscribe(host, key, target) &&
      subscribeOrSetVariables(host, host[key]);
    host.addEventListener('@invalidate', onInvalidate);
    return () => host.removeEventListener('@invalidate', onInvalidate);
  },
};

export const ApolloQuery = {
  variables,
  ...rest
}
Benny Powers
@bennypowers
SOLVED please disregard, it was a test runner issue
Dominik Lubański
@smalluban
@bennypowers Sorry, I didn't noticed your messages. I think I did not receive e-mail notification as well. Few seconds ago I released v4.0. Personally I don't like introducing a lot of breaking changes, but this one was worth it :) Performance boost is huge, and it opens way to add cache mechanism (with change detection) not only to html elements, but also for arbitrary objects - this was needed for my current project - store factory.
Benny Powers
@bennypowers
Cool
Johan Alkstål
@johanalkstal
Good work on the latest release @smalluban
Dominik Lubański
@smalluban
Thanks @johanalkstal !
Jussi Arpalahti
@jussiarpalahti
Hi. I'm doing some custom element experiments. Hybrids seems really great, especially its simplicity and functional feel.
I was wondering how to get elements to respond when attribute is updated. Setting property or changing slotted element content changes accordingly. Attributes apparently initialize element state, but I couldn't find attributeChangedCallback in Hybrids code.
Is there a way to get this working in Hybrids?
Riccardo Scalco
@riccardoscalco
@jussiarpalahti I have the same question. I created a web component implementing a ternary plot (https://github.com/nextbitlabs/ternary-plot), but when the data changes the plot does not update (here you can see my test https://stackblitz.com/edit/ternary-plot-example-random-data). I think I am missing something, I read about the connect and render descriptors, but I am not sure they are the way. @smalluban I really enjoy hybrids, great work! Thanks.
Jussi Arpalahti
@jussiarpalahti
I looked at connect and observe descriptor docs, but they didn't seem to fit here. None of the example apps make use of these features (that I could find). It would be nice to have a definitive sample app with all the bells and whistles in use.
Ah. Now I found it in the docs @riccardoscalco : "An attribute value is used only once when an element connects for the first time to the document. Attributes should be used to set static values in HTML templates. Only properties can dynamically update them"
https://hybrids.js.org/built-in-factories/property#attribute-fallback
Jussi Arpalahti
@jussiarpalahti
My rationale for using setAttribute is dynamic server rendering. I'm using Morphdom Phoenix LiveView style, which means I get HTML string from server and let Morphdom mutate browser DOM into it. This can happen several times per second and is quite performant. Morphdom is sufficiently smart to only change web component's attributes if its place otherwise stays the same. By leveraging attributeChangedCallback my web components can react to these changes and update their state accordingly. Sadly, it looks like with Hybrids this particular use case is not available.
Riccardo Scalco
@riccardoscalco

@jussiarpalahti thanks for the tip.
@smalluban apparently I used a property for the data variable, which is an object. The chart still does not update. The relevant code is the following.

<ternary-plot side="400"></ternary-plot>
<button>show new data</button>

<script>
  const element = document.querySelector('ternary-plot');
  // Set data as a dynamic property.
  element.data = getRandomData(20);

  const button = document.querySelector('button');

  button.addEventListener('click', event => {
    console.log('change data');
    element.data = getRandomData(20);
  });

  function getRandomData (length) {
    ...
  }
</script>

Note that in the component object I did not add data:

export const TernaryPlot = {
    ...
    // there is not a key named data.
}

Here an example https://stackblitz.com/edit/ternary-plot-example-random-data?file=index.html .

Corvince
@Corvince
Hi all and thanks for the wonderful library!
I have a question regarding the property descriptors: If I only set up "observe" to do something, how can I set the initial value? Or more precisely: What is the recommended way?
Of course I can simply set el.name = "value" down the line in JS, but for non-observe descriptors I can simply set the initial property value through html attributes, which I think is a clean and nice way. But this doesn't work if only observe is set. Or is this a bug?
Hmm actually using el.name = value of course triggers the observe function, which I might not want to happen
Dominik Lubański
@smalluban
Sorry, it seems that I don't receive email notification anymore about new messages here (or with few days delay :(). I am going to move to slack soon
I will read your messages and try to answer all your concerns :)
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 :)