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?

Community
  • 1
  • 1
Grokys
  • 16,228
  • 14
  • 69
  • 101
  • 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? – Etherealone Jun 19 '15 at 15:24
  • There is https://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. – Etherealone Jun 19 '15 at 16:51
  • 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. – zerodevx Jun 25 '15 at 08:46
  • @zerodevx: Yes, in the end I changed my app to use pure data binding too. However, this question is still interesting to me. – Grokys Jun 26 '15 at 09:21

8 Answers8

12

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

Arco
  • 723
  • 1
  • 5
  • 8
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>
  • This is nice. Maybe find a way for `push`, `pop`, `shift`, `unshift`, and `splice` too? [Polymer.push prototype](https://github.com/Polymer/polymer/blob/3e96425bf0e0ba49b5f1f2fd2b6008e45a206692/src/standard/notify-path.html#L373) may help maybe. There is an internal `_notifySplice` apparently. No idea how stable is that interface though. – Etherealone Jul 01 '15 at 00:17
  • 2
    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. – Etherealone Jul 01 '15 at 00:23
  • 1
    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. – Grokys Sep 07 '15 at 10:02
  • I couldn't get this to work with Polymer 2.0, any idea what you'd have to update for it to work? – limitlessloop May 15 '17 at 22:55
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.

Etherealone
  • 3,488
  • 2
  • 37
  • 56
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.

Grokys
  • 16,228
  • 14
  • 69
  • 101
  • 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. – zerodevx Jun 26 '15 at 09:55
  • I think this looks a bit overly complicated. So I guess it also replicates the array-changed event fired by `this.push`. – Etherealone Jul 01 '15 at 00:03
  • 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? – limitlessloop May 10 '17 at 01:55
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

ootwch
  • 930
  • 11
  • 32
  • Not as it is currently built, as the Polymer observer does not monitor/propagate change events on array elements. Check http://stackoverflow.com/a/30676440/1878199 for some ideas on how to change this if you really need it. I tend to reduce global variables instead, by adapting the design. – ootwch Dec 12 '15 at 06:50
  • 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. – jkhoffman Feb 27 '17 at 21:47
  • Could this be modified to be used so you can import global variables as a dependency for a given custom element? – limitlessloop May 10 '17 at 01:09
  • Out of curiosity, why are you using `this.getAttribute('key')` instead of just `this.key`? – Ben Davis Sep 16 '17 at 21:29
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.

Alexei Volkov
  • 851
  • 9
  • 11
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).

Salah Saleh
  • 793
  • 9
  • 29
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.

Community
  • 1
  • 1
jkhoffman
  • 199
  • 2
  • 11