Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Add support for ?? in observable bindings",
"packageName": "@ni/fast-element",
"email": "7282195+m-akinc@users.noreply.github.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Target es2022 rather than es2015",
"packageName": "@ni/fast-foundation",
"email": "7282195+m-akinc@users.noreply.github.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Target es2022 rather than es2015",
"packageName": "@ni/fast-react-wrapper",
"email": "7282195+m-akinc@users.noreply.github.com",
"dependentChangeType": "patch"
}
7 changes: 4 additions & 3 deletions packages/utilities/fast-react-wrapper/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
"declarationDir": "dist/dts",
"outDir": "dist/esm",
"experimentalDecorators": true,
"target": "es2015",
"module": "ESNext",
"useDefineForClassFields": false,
"target": "es2022",
"module": "es2020",
"importHelpers": true,
"jsx": "react",
"types": [
Expand All @@ -14,7 +15,7 @@
],
"lib": [
"DOM",
"ES2015"
"ES2022"
]
},
"include": ["src"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe("The Observable", () => {
@observable child = new ChildModel();
@observable child2 = new ChildModel();
@observable trigger = 0;
@observable nullableTrigger: number | null = 0;
@observable value = 10;

childChangedCalled = false;
Expand Down Expand Up @@ -41,9 +42,14 @@ describe("The Observable", () => {
}

@volatile
get andCondition() {
get andConditional() {
return this.trigger && this.value;
}

@volatile
get nullishCoalescingConditional() {
return this.nullableTrigger ?? this.value;
}
}

class ChildModel {
Expand Down Expand Up @@ -121,18 +127,18 @@ describe("The Observable", () => {
it("can list all accessors for an object", () => {
const accessors = Observable.getAccessors(new Model());

expect(accessors.length).to.equal(4);
expect(accessors.length).to.equal(5);
expect(accessors[0].name).to.equal("child");
expect(accessors[1].name).to.equal("child2");
});

it("can list accessors for an object, including the prototype chain", () => {
const accessors = Observable.getAccessors(new DerivedModel());

expect(accessors.length).to.equal(5);
expect(accessors.length).to.equal(6);
expect(accessors[0].name).to.equal("child");
expect(accessors[1].name).to.equal("child2");
expect(accessors[4].name).to.equal("derivedChild");
expect(accessors[5].name).to.equal("derivedChild");
});

it("can create a binding observer", () => {
Expand Down Expand Up @@ -463,7 +469,7 @@ describe("The Observable", () => {
});

it("notifies on changes in a computed && expression", async () => {
const binding = (x: Model) => x.trigger && x.value;
const binding = (x: Model) => x.andConditional;

let wasNotified = false;
const observer = Observable.binding(binding, {
Expand Down Expand Up @@ -497,6 +503,80 @@ describe("The Observable", () => {
expect(value).to.equal(binding(model));
});

it("notifies on changes in a ?? expression", async () => {
const binding = (x: Model) => x.nullableTrigger ?? x.value;

let wasNotified = false;
const observer = Observable.binding(binding, {
handleChange() {
wasNotified = true;
},
});

const model = new Model();
model.nullableTrigger = 0;

let value = observer.observe(model, defaultExecutionContext);
expect(value).to.equal(binding(model));

expect(wasNotified).to.be.false;
model.nullableTrigger = null;

await DOM.nextUpdate();

expect(wasNotified).to.be.true;

value = observer.observe(model, defaultExecutionContext);
expect(value).to.equal(binding(model));

wasNotified = false;
model.value = 20; // nullableTrigger is null, so value change should notify

await DOM.nextUpdate();

expect(wasNotified).to.be.true;

value = observer.observe(model, defaultExecutionContext);
expect(value).to.equal(binding(model));
});

it("notifies on changes in a computed ?? expression", async () => {
const binding = (x: Model) => x.nullishCoalescingConditional;

let wasNotified = false;
const observer = Observable.binding(binding, {
handleChange() {
wasNotified = true;
},
});

const model = new Model();
model.nullableTrigger = 0;

let value = observer.observe(model, defaultExecutionContext);
expect(value).to.equal(binding(model));

expect(wasNotified).to.be.false;
model.nullableTrigger = null;

await DOM.nextUpdate();

expect(wasNotified).to.be.true;

value = observer.observe(model, defaultExecutionContext);
expect(value).to.equal(binding(model));

wasNotified = false;
model.value = 20; // nullableTrigger is null, so value change should notify

await DOM.nextUpdate();

expect(wasNotified).to.be.true;

value = observer.observe(model, defaultExecutionContext);
expect(value).to.equal(binding(model));
});

it("notifies on changes in an || expression", async () => {
const binding = (x: Model) => x.trigger || x.value;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export interface BindingObserver<TSource = any, TReturn = any, TParent = any>
* @public
*/
export const Observable = FAST.getById(KernelServiceId.observable, () => {
const volatileRegex = /(:|&&|\|\||if)/;
const volatileRegex = /(\?\?|:|&&|\|\||if)/;
const notifierLookup = new WeakMap<any, Notifier>();
const queueUpdate = DOM.queueUpdate;
let watcher: BindingObserverImplementation | undefined = void 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,9 @@ export class HTMLBindingDirective extends TargetedHTMLDirective {
* Creates an instance of BindingDirective.
* @param binding - A binding that returns the data used to update the DOM.
*/
public constructor(public binding: Binding) {
public constructor(public binding: Binding, isVolatile?: boolean) {
super();
this.isBindingVolatile = Observable.isVolatileBinding(this.binding);
this.isBindingVolatile = isVolatile ?? Observable.isVolatileBinding(this.binding);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { defaultExecutionContext } from "../observation/observable";
import { css } from "../styles/css";
import type { StyleTarget } from "../styles/element-styles";
import { toHTML, uniqueElementName } from "../__test__/helpers";
import { HTMLBindingDirective } from "./binding";
import { BindingBehavior, HTMLBindingDirective } from "./binding";
import { compileTemplate } from "./compiler";
import type { HTMLDirective } from "./html-directive";
import { html } from "./template";

describe("The template compiler", () => {
describe.only("The template compiler", () => {
function compile(html: string, directives: HTMLDirective[]) {
const template = document.createElement("template");
template.innerHTML = html;
Expand All @@ -25,6 +25,10 @@ describe("The template compiler", () => {
return new HTMLBindingDirective(() => result);
}

function volatileBinding() {
return new HTMLBindingDirective(() => Math.random() > 0.5 ? "greater" : "less");
}

const scope = {};

context("when compiling content", () => {
Expand Down Expand Up @@ -343,6 +347,42 @@ describe("The template compiler", () => {
}
});
});

it(`marks aggregate binding volatile when first binding is volatile and second is not`, () => {
const html = `<a href="beginning ${inline(0)} ${inline(1)}">Link</a>`;
const directives = [volatileBinding(), binding()];
const { fragment, viewBehaviorFactories } = compile(html, directives);

const behavior = viewBehaviorFactories[0].createBehavior(fragment) as BindingBehavior;
expect(behavior.isBindingVolatile).to.be.true;
});

it(`marks aggregate binding volatile when second binding is volatile and first is not`, () => {
const html = `<a href="beginning ${inline(0)} ${inline(1)}">Link</a>`;
const directives = [binding(), volatileBinding()];
const { fragment, viewBehaviorFactories } = compile(html, directives);

const behavior = viewBehaviorFactories[0].createBehavior(fragment) as BindingBehavior;
expect(behavior.isBindingVolatile).to.be.true;
});

it(`marks aggregate binding non-volatile when neither of two bindings are volatile`, () => {
const html = `<a href="beginning ${inline(0)} ${inline(1)}">Link</a>`;
const directives = [binding(), binding()];
const { fragment, viewBehaviorFactories } = compile(html, directives);

const behavior = viewBehaviorFactories[0].createBehavior(fragment) as BindingBehavior;
expect(behavior.isBindingVolatile).to.be.false;
});

it(`marks aggregate binding volatile when both of two bindings are volatile`, () => {
const html = `<a href="beginning ${inline(0)} ${inline(1)}">Link</a>`;
const directives = [volatileBinding(), volatileBinding()];
const { fragment, viewBehaviorFactories } = compile(html, directives);

const behavior = viewBehaviorFactories[0].createBehavior(fragment) as BindingBehavior;
expect(behavior.isBindingVolatile).to.be.true;
});
});

context("when compiling comments", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { _interpolationEnd, _interpolationStart, DOM } from "../dom.js";
import type { Binding, ExecutionContext } from "../observation/observable.js";
import { type Binding, type ExecutionContext, Observable } from "../observation/observable.js";
import { HTMLBindingDirective } from "./binding.js";
import type { HTMLDirective, NodeBehaviorFactory } from "./html-directive.js";

Expand Down Expand Up @@ -53,13 +53,15 @@ function createAggregateBinding(
}

let targetName: string | undefined;
let isVolatile = false;
const partCount = parts.length;
const finalParts = parts.map((x: string | InlineDirective) => {
if (typeof x === "string") {
return (): string => x;
}

targetName = x.targetName || targetName;
isVolatile = isVolatile || Observable.isVolatileBinding(x.binding);
return x.binding;
});

Expand All @@ -73,7 +75,7 @@ function createAggregateBinding(
return output;
};

const directive = new HTMLBindingDirective(binding);
const directive = new HTMLBindingDirective(binding, isVolatile);
directive.targetName = targetName;
return directive;
}
Expand Down
8 changes: 4 additions & 4 deletions packages/web-components/fast-element/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@
"experimentalDecorators": true,
"noImplicitAny": false,
"strictPropertyInitialization": false,
"target": "es2015",
"module": "ESNext",
"useDefineForClassFields": false,
"target": "es2022",
"module": "es2020",
"types": [
"mocha",
"webpack-env",
"web-ie11"
],
"lib": [
"DOM",
"ES2015",
"ES2016.Array.Include"
"ES2022"
]
},
"include": ["src"]
Expand Down
7 changes: 4 additions & 3 deletions packages/web-components/fast-foundation/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
"experimentalDecorators": true,
"suppressImplicitAnyIndexErrors": true,
"strictPropertyInitialization": false,
"useDefineForClassFields": false,
"importsNotUsedAsValues": "error",
"ignoreDeprecations": "5.0",
"target": "es2015",
"module": "ESNext",
"target": "es2022",
"module": "es2020",
"importHelpers": true,
"types": ["mocha", "webpack-env"],
"lib": ["DOM", "ES2015", "ES2016.Array.Include"]
"lib": ["DOM", "ES2022"]
},
"include": ["src"]
}
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"strictNullChecks": true,
"noImplicitAny": true,
"strictPropertyInitialization": true,
"module": "ES6",
"module": "es2020",
"moduleResolution": "node",
"target": "ES6"
"target": "es2022"
}
}