Skip to content

Instantly share code, notes, and snippets.

@Juraji
Last active January 30, 2019 08:15
Show Gist options
  • Save Juraji/b36376b64780f336a48c8f2da9f6c92e to your computer and use it in GitHub Desktop.
Save Juraji/b36376b64780f336a48c8f2da9f6c92e to your computer and use it in GitHub Desktop.
Angular: Bind component property to a query parameter
import { Component, OnInit } from "@angular/core";
@Component({
selector: "app-query-parameter-binding-example",
template: "<p>Property 1: {{property1}}</p><p>Property 2: {{property2}}</p>"
})
export class QueryParameterBindingExampleComponent implements OnInit {
/**
* When updated is reflected as query parameter "property-1=[value]" in url
* Getter will return query parameter value
* Setter will update query parameter value
*/
@QueryParameterBinding("property-1")
public get property1(): string {
// Property getter
}
public set property1(value: string) {
// Property setter
}
/**
* When updated is reflected as query parameter "property-2=[value]" in url
*/
@QueryParameterBinding("property-2")
public property2: string;
public ngOnInit() {
// Set an interval to update the bound values,
// demonstrating setting values and reflecting in query parameters
let counter = 0;
setInterval(() => {
counter++;
this.property1 = counter;
this.property2 = counter;
}, 1000);
}
}
import { QueryParameterBinding } from "./query-parameter-binding";
class TestComponent {
private _getSetProperty;
public get getSetProperty() {
return this._getSetProperty;
}
public set getSetProperty(value) {
this._getSetProperty = value;
}
public primitiveProperty: string;
}
describe("QueryParameterBinding", () => {
const decoratorFn = QueryParameterBinding("test");
it("should support get/set and primitive properties", () => {
const cmpObject = new TestComponent();
const getSetPropertyDescriptor = Object.getOwnPropertyDescriptor(cmpObject, "getSetProperty");
// Calling factory with only an object and property name (Primitive properties)
// should not throw a (compiler) error
decoratorFn(cmpObject, "primitiveProperty");
// Calling factory with property descriptor should not throw a (compiler) error
decoratorFn(cmpObject, "getSetProperty", getSetPropertyDescriptor);
});
it("should update the property to the current query", () => {
// Todo: Write tests
});
it("should reflect property value changes to the query parameter", () => {
// Todo: Write tests
});
it("should support two-way binding (Using the *Change property if it exists)", () => {
// Todo: Write tests
});
});
import { EventEmitter } from "@angular/core";
/**
* Bind a property or method to an url query parameter.
*
* Possible compiler warnings:
* - TS2345: Decorator is being used on a non-string type property
* This error is not thrown on primitive properties, beware unexpected behaviour!
* - TS1238, TS2559 or TS2345: Decorator does not support usage on classes, constructor parameters or methods
*
* @param name The name for the query parameter
* @decorator
* @author Juraji 2019 (https://github.com/Juraji)
*/
export function QueryParameterBinding(name: string) {
const getQMValue = (): string => {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has(name)) {
return urlParams.get(name);
} else {
return null;
}
};
const setQMValue = (v: string) => {
const urlParams = new URLSearchParams(window.location.search);
if (v == null) {
if (urlParams.has(name)) {
urlParams.delete(name);
}
} else {
urlParams.set(name, v);
}
const newPath = window.location.pathname + "?" + urlParams.toString();
window.history.replaceState(null, "", newPath);
};
return (target: any, key: string, descriptor?: TypedPropertyDescriptor<string>): any => {
// if query parameter is present update descriptor to reflect value
if (descriptor && descriptor.set) {
const qmValue = getQMValue();
if (qmValue != null) {
descriptor.set.apply(target, [qmValue]);
}
}
const proxyDescriptor: TypedPropertyDescriptor<string> = {
enumerable: true,
configurable: true,
get: function () {
let q = getQMValue();
if (descriptor && descriptor.get) {
// If there was a descriptor sync the parameter
// and its value is different from the query parameter
// update the query parameter
const p = descriptor.get.apply(this);
if (q !== p) {
setQMValue(p);
q = p;
}
}
return q;
},
set: function (v) {
setQMValue(v);
if (descriptor && descriptor.set) {
// If there was a descriptor set its value as well
descriptor.set.apply(this, [v]);
}
}
};
// Two-way binding support:
// if change property exists, subscribe to it, updating the query parameter on change
const changePropKey = `${key}Change`;
if (target.hasOwnProperty(changePropKey) && target[changePropKey] instanceof EventEmitter) {
(target[changePropKey] as EventEmitter<string>)
.subscribe(v => setQMValue(v));
}
return proxyDescriptor;
};
}
@Juraji
Copy link
Author

Juraji commented Jan 29, 2019

Todo: Write tests/figure out how to mock usage of window.location

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment