Polymer 1.0 Global Variables
Asked Answered
J

8

31

In Polymer 0.5 the advice on globals was as outlined in this question/answer:

Polymer global variables

However in Polymer 1.0 this doesn't seem to work. Change notifications are not automatically generated on the underlying model, they are generated on the <dom-module> instead which means that change notifications will be generated on only one of the <app-globals>.

What is the recommended way of implementing this pattern in Polymer 1.0?

Jaimie answered 15/6, 2015 at 16:3 Comment(4)
This is really an issue. I suppose we need to use an external variable observing solution and call this.notifyPath in the element manually. Is there any lightweight popular object observing framework?Merrygoround
There is github.com/polymer/observe-js so I think it will do the job. I wonder if it will be supported. I guess they should write a tutorial on what to use for this situation.Merrygoround
Bit late to comment, but I was wondering what exactly is your use-case? I too am in the midst of upgrading a fairly complex 0.5 app to 1.0, and found that I could reorganise using pure data-binding instead.Javelin
@zerodevx: Yes, in the end I changed my app to use pure data binding too. However, this question is still interesting to me.Jaimie
S
12

Polymer element <iron-meta> is also an option. For me this was the easiest solution.

Superfamily answered 31/7, 2015 at 13:18 Comment(0)
U
11

I've extended Etherealones' solution to work as a Behavior, and to extend Polymers "set" and "notifyPath" methods to trigger the updates automatically. This is as close as i could get to a true databinding across components/elements:

globals-behavior.html:

<script>
var instances = [];
var dataGlobal = {};

var GlobalsBehaviour = {

  properties: {
    globals: {
      type: Object,
      notify: true,
      value: dataGlobal
    }
  },

  ready: function() {
    var _setOrig = this.set;
    var _notifyPathOrig = this.notifyPath;
    this.set = function() {
      _setOrig.apply(this, arguments);
      if (arguments[0].split(".")[0] === "globals") {
        this.invokeInstances(_notifyPathOrig, arguments);
      }
    };
    this.notifyPath = function(path, value) {
      _notifyPathOrig.apply(this, arguments);
      if (arguments[0].split(".")[0] === "globals") {
        this.invokeInstances(_notifyPathOrig, arguments);
      }
    };
  },

  invokeInstances: function(fn, args) {
    var i;
    for (i = 0; i < instances.length; i++) {
      instance = instances[i];
      if (instance !== this) {
        fn.apply(instance, args);
      }
    }
  },

  attached: function() {
    instances.push(this);
  },

  detached: function() {
    var i;
    i = instances.indexOf(this);
    if (i >= 0) {
      instances.splice(i, 1);
    }
  }
};

</script>

And in all polymer elements that should have access to the globals variable:

  <script>
    Polymer({
      is: 'globals-enabled-element',
      behaviors: [GlobalsBehaviour]
    });
  </script>

Examples:

  1. I have posted a full example as a Gist on Github
  2. Here's a snippet to see it in action:

    <!DOCTYPE html>
    <html>
      <head>
        <title>Globals Behavior Example</title>
        <link rel="import" href="//rawgit.com/Polymer/polymer/master/polymer.html">
        <dom-module id="globals-enabled-element">
          <template>
            <input type="text" value="{{globals.my_variable::input}}">
          </template>
          <script>
            var instances = [];
            var dataGlobal = {};
            
            var GlobalsBehaviour = {
              
              properties: {
                globals: {
                  type: Object,
                  notify: true,
                  value: dataGlobal
                }
              },
              
              ready: function() {
                var _setOrig = this.set;
                var _notifyPathOrig = this.notifyPath;
                this.set = function() {
                  _setOrig.apply(this, arguments);
                  if (arguments[0].split(".")[0] === "globals") {
                    this.invokeInstances(_notifyPathOrig, arguments);
                  }
                };
                this.notifyPath = function(path, value) {
                  _notifyPathOrig.apply(this, arguments);
                  if (arguments[0].split(".")[0] === "globals") {
                    this.invokeInstances(_notifyPathOrig, arguments);
                  }
                };
              },
              
              invokeInstances: function(fn, args) {
                var i;
                for (i = 0; i < instances.length; i++) {
                  instance = instances[i];
                  if (instance !== this) {
                    fn.apply(instance, args);
                  }
                }
              },
              
              attached: function() {
                instances.push(this);
              },
              
              detached: function() {
                var i;
                i = instances.indexOf(this);
                if (i >= 0) {
                  instances.splice(i, 1);
                }
              }
            };
          </script>
          <script>
            Polymer({
              is: 'globals-enabled-element',
              behaviors: [GlobalsBehaviour]
            });
          </script>
        </dom-module>
      </head>
      <body>
        <template is="dom-bind">
          <p>This is our first polymer element:</p>
          <globals-enabled-element id="element1"></globals-enabled-element>
          <p>And this is another one:</p>
          <globals-enabled-element id="element2"></globals-enabled-element>
        </template>
      </body>
    </html>
Unbowed answered 30/6, 2015 at 3:42 Comment(4)
This is nice. Maybe find a way for push, pop, shift, unshift, and splice too? Polymer.push prototype may help maybe. There is an internal _notifySplice apparently. No idea how stable is that interface though.Merrygoround
Also, I'd rather provide the global variable to element myself, maybe you should expect the variable name _globals and use console.warn in Behavior.ready if it does not exist. A single global global does not sound good.Merrygoround
I agree - implementing it as a behavior suggests that it's ready to be used by different components, but as far as I can tell, that is not the case here.Jaimie
I couldn't get this to work with Polymer 2.0, any idea what you'd have to update for it to work?Accepter
M
4

I have implemented a pattern like iron-signals uses for this purpose. So the basic principle is that you manually notify other instances when an update occurs.

Consider this:

<dom-module id="x-global">
<script>
(function() {
  var instances = [];

  var dataGlobal = {};

  Polymer({
    is: 'x-global',

    properties: {
      data: {
        type: Object,
        value: dataGlobal,
      },
    },

    attached: function() {
      instances.push(this);
    },

    detached: function() {
      var i = instances.indexOf(this);
      if (i >= 0) {
        instances.splice(i, 1);
      }
    },

    set_: function(path, value) {
      this.set(path, value);

      instances.forEach(function(instance) {
        if (instance !== this) { // if it is not this one
          instance.notifyPath(path, value);
        }
      }.bind(this));
    },

    notifyPath_: function(path, value) {
      instances.forEach(function(instance) {
        instance.notifyPath(path, value);
      });
    },

    fire_: function(name, d) {
      instances.forEach(function(instance) {
        instance.fire(name, d);
      });
    },
  });
})();
</script>
</dom-module>

You will simple call the version that have an underscore suffix like fire_ when you are firing an event. You can even create a Polymer Behavior of some sort with this pattern I guess.

Beware that preceding underscore properties are already used by Polymer so don't go ahead and convert these to _fire.

P.S.: I didn't look around to solve how to reflect the notification of this.push(array, value); as I don't need it. I don't know if it's possible this way. Should go find Polymer.Base.push.

Merrygoround answered 24/6, 2015 at 16:11 Comment(1)
Great approach! I've extended it to be used as a Behavior, will post here as an answer.Unbowed
J
4

Sjmiles, one of Polymer's creators just posted the following snippet to the Polymer slack room as an example of shared data:

<!doctype html>
<html>
<head>

  <meta charset="utf-8">

  <meta name="description" content="shared-data element and repeats">

  <base href="http://milestech.net/components/">

  <script href="webcomponentsjs/webcomponents-lite.min.js"></script>
  <link href="polymer/polymer.html" rel="import">

</head>
<body>

  <demo-test></demo-test>

  <script>

    (function() {
      var private_data = [{name: 'a'}, {name: 'b'}, {name: 'c'}];
      Polymer({
        is: 'private-shared-data',
        properties: {
          data: {
            type: Object,
            notify: true,
            value: function() {
              return private_data;
            }
          }
        }
      });
    })();

    Polymer({
      is: 'xh-api-device',
      properties: {
        data: {
          type: Array,
          notify: true
        },
        _share: {
          value: document.createElement('private-shared-data')
        }
      },
      observers: [
        'dataChanged(data.*)'
      ],
      ready: function() {
        this.data = this._share.data;
        this.listen(this._share, 'data-changed', 'sharedDataChanged');
      },
      dataChanged: function(info) {
        this._share.fire('data-changed', info, {bubbles: false});
      },
      sharedDataChanged: function(e) {
        this.fire(e.type, e.detail);
      },
      add: function(name) {
        this.push('data', {name: name});
      }
    });

  </script>

  <dom-module id="demo-test">
    <template>

      <h2>One</h2>

      <xh-api-device id="devices" data="{{data}}"></xh-api-device>

      <template is="dom-repeat" items="{{data}}">
        <div>name: <span>{{item.name}}</span></div>
      </template>

      <h2>Two</h2>

      <xh-api-device data="{{data2}}"></xh-api-device>

      <template is="dom-repeat" items="{{data2}}">
        <div>name: <span>{{item.name}}</span></div>
      </template>

      <br>
      <br>

      <button on-click="populate">Populate</button>

    </template>
    <script>
      Polymer({
        populate: function() {
          this.$.devices.add((Math.random()*100).toFixed(2));
          // this works too
          //this.push('data', {name: (Math.random()*100).toFixed(2)});
        }
      });
    </script>
  </dom-module>

</body>
</html>

I've actually moved my app to using simple data binding, so I'm not sure of the validity of this approach, but maybe it would be useful to someone.

Jaimie answered 26/6, 2015 at 9:27 Comment(3)
Thanks for posting the example! At first I thought it might be an anti-pattern to use 0.5-like global vars (since no mention of this was made in 1.0 docs), so I made the rather painful change to pure data-binding in my app. Glad that there's a way to achieve global var functionality in 1.0 too.Javelin
I think this looks a bit overly complicated. So I guess it also replicates the array-changed event fired by this.push.Merrygoround
Made a bit more sense to me with regards to my basic need of sharing variables from one source to other elements. I'm guessing you could change data from an array to an object if you needed to?Accepter
R
4

I have tried to improve on Alexei Volkov's answer, but I wanted to define the global variables separately. Instead of the getters/setters I used the observer property and saved the key together with the instances.

The usage is:

<app-data  key="fName" data="{{firstName}}" ></app-data>

whereas the keyproperty defines the name of the global variable.

So for example you can use:

<!-- Output element -->
<dom-module id="output-element" >
  <template>
    <app-data key="fName" data="{{data1}}" ></app-data>
    <app-data key="lName" data="{{data2}}" ></app-data>
    <h4>Output-Element</h4>
    <div>First Name: <span>{{data1}}</span></div>
    <div>Last Name: <span>{{data2}}</span></div>
  </template>
</dom-module>

<script>Polymer({is:'output-element'});</script>

Definition of the <app-data>dom module:

<dom-module id="app-data"></dom-module>
<script>
(function () {
    var instances = [];
    var vars = Object.create(Polymer.Base);

    Polymer({
        is: 'app-data',
        properties: {
           data: {
                type: Object,
                value: '',
                notify: true,
                readonly: false,
                observer: '_data_changed'
            },
          key: String
        },
        created: function () {
          key = this.getAttribute('key');
          if (!key){
            console.log(this);
            throw('app-data element requires key');
          }

          instances.push({key:key, instance:this});
        },

        detached: function () {
            key = this.getAttribute('key');
            var i = instances.indexOf({key:key, instance:this});
            if (i >= 0) {
                instances.splice(i, 1);
            }
        },

      _data_changed: function (newvalue, oldvalue) {
        key = this.getAttribute('key');
        if (!key){
            throw('_data_changed: app-data element requires key');
            }
        vars.set(key, newvalue);

        // notify the instances with the correct key
        for (var i = 0; i < instances.length; i++) 
        {
          if(instances[i].key == key)
          {
            instances[i].instance.notifyPath('data', newvalue);
          }
        }
      }


    });
})();
</script>

Fully working demo is here: http://jsbin.com/femaceyusa/1/edit?html,output

Rapeseed answered 2/8, 2015 at 11:6 Comment(4)
Not as it is currently built, as the Polymer observer does not monitor/propagate change events on array elements. Check https://mcmap.net/q/471529/-polymer-how-do-i-attach-an-observer-to-an-array for some ideas on how to change this if you really need it. I tend to reduce global variables instead, by adapting the design.Rapeseed
Using this solution, I ran into a race condition situation with lazy-loaded components. As posted, lazy-loaded components are not initialized with the value from the shared data. See my post for a patch.Alongside
Could this be modified to be used so you can import global variables as a dependency for a given custom element?Accepter
Out of curiosity, why are you using this.getAttribute('key') instead of just this.key?Ot
F
1

I've combined all suggestions above into the following global polymer object

<dom-module id="app-data">
</dom-module>
<script>
    (function () {
        var instances = [];
        var vars = Object.create(Polymer.Base);
        var commondata = {
            get loader() {
                return vars.get("loader");
            },
            set loader(v) {
                return setGlob("loader", v);
            }
        };

        function setGlob(path, v) {
            if (vars.get(path) != v) {
                vars.set(path, v);
                for (var i = 0; i < instances.length; i++) {
                    instances[i].notifyPath("data." + path, v);
                }
            }
            return v;
        }

        Polymer({
            is: 'app-data',
            properties: {
                data: {
                    type: Object,
                    value: commondata,
                    notify: true,
                    readonly: true
                }
            },
            created: function () {
                instances.push(this);
            },

            detached: function () {
                var i = instances.indexOf(this);
                if (i >= 0) {
                    instances.splice(i, 1);
                }
            }
        });
    })();
</script>

and use it elsewere like

<dom-module id="app-navigation">
    <style>

    </style>
    <template>
        <app-data id="data01" data="{{data1}}" ></app-data>
        <app-data id="data02" data="{{data2}}"></app-data>
        <span>{{data1.loader}}</span>
        <span>{{data2.loader}}</span>
    </template>

</dom-module>
<script>

    (function () {
        Polymer({
            is: 'app-navigation',
            properties: {
            },
            ready: function () {
                this.data1.loader=51;
            }
        });
    })();
</script>

Changing either data1.loader or data2.loader affects other instances. You should to extend commondata object to add more global properties like it shown with loader property.

Farewell answered 3/7, 2015 at 15:32 Comment(0)
P
1

It is much easier to achieve the same effect of global variables if you wrapped your application in a template. Watch the explanation in this video (I linked to the exact minute and second where the concept is explained).

Plagioclase answered 8/8, 2015 at 9:15 Comment(1)
Your link did not show up on the post. Please repost with the link.Dialogism
A
1

Using ootwch's solution, I ran into a race condition situation with lazy-loaded components.

As posted, lazy-loaded components are not initialized with the value from the shared data.

In case anyone else runs into the same problem, I think I fixed it by adding a ready callback like this:

ready: function() {
  const key = this.getAttribute('key')
  if (!key) {
    throw new Error('cm-app-global element requires key')
  }

  const val = vars.get(key)
  if (!!val) {
    this.set('data', val)
  }
},

Hope this saves someone some pain.

Alongside answered 27/2, 2017 at 21:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.