{"id":13748635,"url":"https://github.com/google/uniflow-polymer","last_synced_at":"2025-05-09T11:31:04.033Z","repository":{"id":57741280,"uuid":"75316690","full_name":"google/uniflow-polymer","owner":"google","description":"UniFlow for Polymer","archived":true,"fork":false,"pushed_at":"2018-05-15T20:46:24.000Z","size":5003,"stargazers_count":166,"open_issues_count":4,"forks_count":21,"subscribers_count":19,"default_branch":"master","last_synced_at":"2025-04-19T22:27:42.679Z","etag":null,"topics":["mixins","polymer","unidirectional-data-flow"],"latest_commit_sha":null,"homepage":"https://google.github.io/uniflow-polymer/","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/google.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-12-01T17:36:31.000Z","updated_at":"2025-04-11T06:05:31.000Z","dependencies_parsed_at":"2022-08-29T08:50:37.419Z","dependency_job_id":null,"html_url":"https://github.com/google/uniflow-polymer","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/google%2Funiflow-polymer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/google%2Funiflow-polymer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/google%2Funiflow-polymer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/google%2Funiflow-polymer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/google","download_url":"https://codeload.github.com/google/uniflow-polymer/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253240350,"owners_count":21876593,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["mixins","polymer","unidirectional-data-flow"],"created_at":"2024-08-03T07:00:46.225Z","updated_at":"2025-05-09T11:31:03.729Z","avatar_url":"https://github.com/google.png","language":"HTML","funding_links":[],"categories":["Uncategorized","Patterns"],"sub_categories":["Uncategorized","Managing State"],"readme":"# UniFlow for Polymer 2.x\n\nSet of mixins to enable uni-directional data flow in Polymer application.\n\n**Important!**\n\n*This library was developed as part of internal project at Google and isn't directly affiliated with the Polymer project (although Polymer team has provided some good feedback on UniFlow implementation).*\n\n## History \u0026 Overview\n\nWhen you start working on a new Polymer application, it's easy to start and build the first few elements, and make them communicate via events and data binding, so everything looks nice and rosy. However, as the number and complexity of elements grows, it becomes increasingly difficult to manage relationships between them, trace where/when the data changes happened, and debug the problems. So this project started as an attempt by our team at Google to find a good way to architect large Polymer application. \n\nInspired by React's community Flux (and, later, Redux) architecture, we implemented a unidirectional data flow pattern (data down, events up) for Polymer. We found that when using UniFlow application code becomes more streamlined (e.g. it is clear what the responsibilities of each element are) and much easier to manage; the code has fewer bugs, and debugging is a lot more efficient. Adding new functionality no longer exponentially increases complexity.\n\nThis project was also inspired by Backbone Marionette. Backbone.js back in the days of its glory was a great library that provided a nice set of building blocks for building JavaScript applications. However, it left much of the application design, architecture and scalability to the developer, including memory management, view management, and more. Marionette brought an application architecture to Backbone, along with built in view management and memory management. It was designed to be a lightweight and flexible library of tools that sits on top of Backbone, providing the framework for building a scalable application. Uniflow strives to achieve similar goal for Polymer. \n\nWe feel that Polymer, and web components in general, is a great concept that takes interoperability and encapsulation in Web development to the next level. But it lacked the patterns for building large and complex applications, and this is the void we expect UniFlow to fill. It is still in beta, so breaking changes may be happening before the first release. However, we believe that abstractions implemented in the library can be useful for Polymer community, so we encourage people to try, fork, ask questions, send  comments, and submit pull requests.\n\n## Applicability\n\nThis library implements the architectural pattern called 'unidirectional data flow'. It works best if application logic involves complicated data management, when multiple elements need to have access to or modify the same data. Even though the pattern can be implemented just using built-in Polymer concepts, such as custom events and data binding, the UniFlow library provides a useful set of tools and abstractions, and helps to structure application code.\n\n## Implementation\n\nUniFlow is implemented as a set of mixins that developers apply to their elements. It is assumed that each application has a singleton application element that maintains state of entire application. Each element that needs access to the data is bound, directly or indirectly, to sub-tree of application state tree. Two way data binding is never used to send data up, from child to parent, so only parent elements send data to children using one way data binding. Child elements, in turn, send the events (emit actions) responding to user actions, indicating that the data may need to be modified. Special non-visual elements called action dispatchers mutate the data, then all elements listening to the data changes render new data. \n\n# API Documentation\n\n## Action Dispatcher\n\nUse UniFlow.ActionDispatcher for non-visual elements that process actions emitted by visual\nelements. Action dispatchers usually placed at the application level. Each action dispatcher\nelement gets a chance to process the action in the order the elements are present in the\nDOM tree. It is important that action dispatcher elements get two-way data binding to\napplication state as follows:\n\n   \u003caction-dispatcher state=\"{{state}}\"\u003e\u003c/action-dispatcher\u003e\n\nAction dispatcher elements can include nested action dispatchers, so you can have a\nhierarchical organization of action dispatchers.\n\n### Example:\n\n#### HTML:\n```html\n\u003cdom-module id=\"parent-dispatcher\"\u003e\n\u003ctemplate\u003e\n  \u003cchild-dispatcher-a state=\"{{state}}\"\u003e\u003c/child-dispatcher-a\u003e\n  \u003cchild-dispatcher-b state=\"{{state}}\"\u003e\u003c/child-dispatcher-b\u003e\n\u003c/template\u003e\n\u003c/dom-module\u003e\n```\n\n#### JavaScript:\n\n```javascript\nclass ParentDispatcher extends UniFlow.ActionDispatcher(Polymer.Element) {\n\n  static get is() { return 'parent-dispatcher'; }\n  \n  MY_ACTION(detail) {\n   // do MY_ACTION processing here\n   // return false if you want to prevent other action dispatchers from\n   // further processing of this action\n  };\n}\n\ncustomElements.define(ParentDispatcher.is, ParentDispatcher);\n```\n\n## Action Emitter\n\nWhenever element needs to emit an action, this mixin should be used. Action object must always include type property.\n\n## Application State\n\nAssign this mixin to your main application element. It provides global\nstate and functionality to maintain individual elements states. This mixin\nis responsible for notifying all state-aware elements about their state\nchanges (provided those elements have `statePath` property defined).\nOnly one element in the application is supposed to have this mixin.\n\n### Example:\n\n#### HTML:\n```html\n\u003ctemplate\u003e\n  \u003c!-- action dispatchers in the order of action processing --\u003e\n  \u003caction-dispatcher-a state=\"{{state}}\"\u003e\u003c/action-dispatcher-a\u003e\n  \u003caction-dispatcher-b state=\"{{state}}\"\u003e\u003c/action-dispatcher-b\u003e\n  \n  \u003c!-- state-aware elements --\u003e\n  \u003csome-element state-path=\"state.someElement\"\u003e\u003c/some-element\u003e\n\u003c/template\u003e\n```\n#### JavaScript:\n\n```javascript\nclass MyApp extends UniFlow.ApplicationState(Polymer.Element) {\n\n  static get is() { return 'my-app'; }\n\n  connectedCallback() {\n    super.connectedCallback();\n    this.state = {\n      someElement: {}\n    }\n  }\n}\n\ncustomElements.define(MayApp.is, MyApp);\n```\n\nIn the example above, `\u003csome-element\u003e` will receive notification of any changes to the state,\nas if it was declared as follows:\n\n```html\n\u003csome-element state=\"[[state]]\"\u003e\u003c/some-element\u003e\n```\n\nAlso, if `\u003csome-element\u003e` has `propertyA`, on element attach this property will be assigned\nthe value of `state.someElement.propertyA`, and receive all notification of the property change\nwhenever the corresponding data in state tree changes. This essentially translates to following\ndeclaration:\n\n```html\n\u003csome-element state=\"[[state]]\"\n              propertyA=\"[[state.someElement.propertyA]]\"\u003e\n\u003c/some-element\u003e\n```\n\nNote that data binding is one-way in both cases. Although state-aware elements can modify their\nown state, it is considered their private state and no other elements will be notified of those\nchanges.\n\n## List View\n\nThis mixin used by elements that need to render multiple models backed\nby 'list' array. You may want to use ModelView to render individual\nmodels in the list. The mixin supports element selection by setting predefined\n$selected property on list elements.\n\n### Example:\n\n#### HTML:\n\n```html\n\u003cul\u003e\n  \u003ctemplate id=\"list-template\" is=\"dom-repeat\" items=\"[[list]]\"\u003e\n    \u003cli id=\"[[item.id]]\"\u003e\n      \u003cpaper-checkbox checked=\"{{item.$selected}}\"\u003e\n      \u003cmodel-view state-path=\"[[statePath]].list.#[[index]]\"\u003e\u003c/model-view\u003e\n    \u003c/li\u003e\n  \u003c/template\u003e\n\u003c/ul\u003e\nSelected: [[selectedCount]] items\n\u003cpaper-button on-tap=\"onDeleteTap\"\u003eDelete\u003c/paper-button\u003e\n```\n\n#### JavaScript:\n\n```javascript\nclass ListElement extends Polymer.GestureEventListeners(UniFlow.ListView(UniFlow.StateAware(Polymer.Element))) {\n\n  static get is() { return \"list-element\"; }\n\n  onDeleteTap() {\n    this.deleteSelected();\n  }\n\n}\n\ncustomElements.define(ListElement.is, ListElement);\n```\n\nIn the example above list view element is also state-aware, meaning it has its own place\nin the application state tree. Assuming it has been declared as follows:\n\n```html\n\u003clist-element state-path=\"state.listElement\"\u003e\u003c/list-element\u003e\n```\n\nit will be rendering `state.listElement.list` and observing changes to it. Each `model-view`\nwithin dom-repeat template will have `state-path` property  set to\n`state.listElement.list.#\u003cindex\u003e`  where `index` is the element's index in the array.\n\n## Model View\n\nElement rendering data represented by a single object (model) in the\napplication state should use ModelView mixin. Model View is a powerful\nconcept that encapsulates model data (likely the data received from the\nserver and to be persisted to the server if modified as a result of user\nactions), status (validity of the data, flag that data was modified,\nnotifications for the user, etc.). Auxiliary data supplied by action\ndispatchers and needed for display purposes or element's logic\nshould be defined as element’s properties. Same applies to data\ncreated/modified by the element but not intended to be persisted.\nIf `StateAware` mixin is used along with `ModelView`, you can take advantage\nof statePath property that indicates path to the element's state in the\napplication state tree. Whenever any data is mutated by action dispatchers\nat statePath or below, the element will receive notification of its\nproperties' change (even if there is no explicit binding for those\nproperties). See `UniFlow.StateAware` for more details and example.\nModelView mixin defines some properties that are intended to be overridden\nin the elements:\n\n+ `validation` property allows to specify validation rules\nthat will be applied when validateModel() method is called. As a result of\nthis method validation status will be updated to indicate result for each\nmodel field that has validation rule associated with it.\n+ `saveAction` property indicates which action should be emitted when\nsaveModel method is called to perform save of the model.\n+ `getMessage` should be overridden with the function returning message\nstring for given error code (to translate validation error code to message)\n\n\n### Example:\n\n#### HTML:\n\n```html\n\u003ctemplate\u003e\n Model: [[model.id]]\n \u003cpaper-input value=\"{{model.name}}\"\n              label=\"Name\"\n              invalid=\"[[status.validation.name.invalid]]\"\n              error-message=\"[[status.validation.name.errorMessage]]\"\u003e\n \u003c/paper-input\u003e\n \u003cpaper-button on-tap=\"onSaveTap\"\u003eSave\u003c/paper-button\u003e\n\u003c/template\u003e\n```\n\n#### JavaScript:\n\n```javascript\nclass MyModel extends Polymer.GestureEventListeners(UniFlow.ModelView(Polymer.Element)) {\n\n  static get is() { return \"my-model\"; }\n  \n  get saveAction() { return 'MY_SAVE'; }\n  \n  get validation() { \n    return {\n      name: (value) =\u003e {\n        if (!value || !value.trim()) {\n          return 'Name is not specified';\n        }\n      }\n    }\n  }\n  \n  connectedCallback() {\n   super.connectedCallback();\n   this.fetchData();\n  },\n  \n  fetchData() {\n   this.emitAction({\n     type: 'MY_FETCH',\n     path: 'model'\n   });\n  },\n  \n  onSaveTap() {\n   this.validateAndSave();\n  }\n}\n\ncustomElements.define(MyModel.is, MyModel);\n```\n\nIn the example above model view has input field for `name` property and Save button. On\nelement attach the action is emitted to fetch the model's data. Note that in `emitAction()` method\nthe path is specified as `'model'`. ActionEmitter mixin is responsible of expanding the path\nwith element's state path, ensuring that when action dispatcher gets to process the action, the\npath contains full path in the state tree. So assuming that `my-model` is declared as follows:\n\n```html\n\u003cmy-model state-path=\"state.myModel\"\u003e\u003c/my-model\u003e\n```\n\nthe path in `MY_FETCH` action gets expanded to `state.myModel.model`.\n\n`validation` property is an object that contains methods for fields validation. The keys in\nthis object should match model field names, the values are validation methods. Method receives\ncurrent value of the field and should return non-falsy value (string or error code) if the value\nof the field didn't pass validation. `status.validation` object will be populated with the results\nof validation with the keys matching field names and values being objects containing two fields:\n- `invalid`: true when the value is not valid\n- `errorMessage`: the message to show to user\n\n\nSo in the example above if user clicks on Save button with name not entered, they will get\n'Name is not specified' error message on the input element. When the name is non-empty, validation\nwill pass and `MY_SAVE` action will be emitted with model passed as a parameter and `'model'` as\npath.\n\n## State Aware\n\n Key mixin that must be assigned to all elements that need to access\n application state and/or have access to the application element. The element is\n notified of any changes to application's state, as well as all its properties\n when they're modified by state mutator elements. `state-path` property must\n be used to identify path to element's state in application state tree. \n\n\n### Example:\n\n#### HTML:\n\n```html\n\u003ctemplate\u003e\n \u003cdiv\u003eValue A: [[state.valueA]]\u003c/div\u003e\n \u003cdiv\u003eValue B: [[valueB]]\u003c/div\u003e\n\u003c/template\u003e\n```\n\n#### JavaScript:\n\n```javascript\nclass MyElement extends UniFlow.StateAware(Polymer.Element) {\n\n  static get is() { return 'my-element'; }\n  \n  properties: {\n    valueB: String\n  }\n}\n\ncustomElements.define(MyElement.is, MyElement);\n```\n\nWhen above element is declared as follows:\n\n```html\n\u003cmy-element state-path=\"state.myElement\"\u003e\u003c/my-element\u003e\n```\n\nit will be notified about changes (and render those) to `state.valueA` or\n`state.myElement.valueB` in action dispatchers or other state mutating\nelements.\n\n## State Mutator\n\nSome non-visual elements, like action dispatchers, need to modify application\nstate, in which case they should have this mixin assigned. Implements state-\naware and re-declares state property with notify attribute. State mutator elements\nare only supposed to exist at the application level.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgoogle%2Funiflow-polymer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgoogle%2Funiflow-polymer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgoogle%2Funiflow-polymer/lists"}