UNPKG

@ribajs/core

Version:

Core module of Riba.js

1,117 lines (946 loc) 33.5 kB
import { vi } from 'vitest'; import { Riba, View, Binder } from "./index.js"; import { Data } from "../spec/lib/moch.data.js"; import { dotAdapter } from "./adapters/index.js"; import { TextBinder, HtmlBinder, EachStarBinder, ValueBinder, AddClassBinder, } from "./binders/index.js"; import { Formatter, Adapter } from "./types/index.js"; describe("riba.Binder", () => { const riba = new Riba(); riba.module.adapter.register(dotAdapter); riba.module.binder.register(TextBinder); riba.module.binder.register(HtmlBinder); riba.module.binder.register(ValueBinder); riba.module.binder.register(EachStarBinder); riba.module.binder.register(AddClassBinder); let model: object; let el: HTMLElement; let view: View; let binding: TextBinder | ValueBinder; let originalPrefix: string[]; let adapter: Adapter; let routineFn; beforeEach(() => { originalPrefix = riba.prefix; riba.prefix = ["data"]; adapter = riba.adapters["."]; el = document.createElement("div"); el.setAttribute("data-text", "obj.name"); view = riba.bind(el, { obj: { name: "test" } }); binding = view.bindings[0] as TextBinder; model = binding.model; }); afterEach(() => { riba.prefix = originalPrefix; }); it("gets assigned the proper binder routine matching the identifier", () => { routineFn = binding.name; expect(routineFn).toEqual("text"); expect(routineFn).toEqual(riba.binders.text.key); }); describe("when bind to non configurable properties", () => { let data: any; beforeEach(() => { data = { name: "x", items: [], }; Object.defineProperty(data, "name", { enumerable: true, configurable: false, writable: true, value: "y", }); el.setAttribute("data-show", "obj.items.length"); }); it("does not throw", () => { expect(() => { riba.bind(el, { obj: data }); }).not.toThrow(); }); }); describe("with multiple prefixes", () => { beforeEach(() => { riba.prefix = ["foo", "bar", "any"]; // Positive test el.setAttribute("foo-a", "'a'"); el.setAttribute("bar-b", "'b'"); el.setAttribute("any-c", "'c'"); // Negative test el.setAttribute("rv-d", "'d'"); }); it("All prefixes should be applied", () => { expect(() => { riba.bind(el, {}); }).not.toThrow(); // Positive test expect(el.getAttribute("a")).toEqual("a"); expect(el.getAttribute("b")).toEqual("b"); expect(el.getAttribute("c")).toEqual("c"); // Negative test expect(el.getAttribute("d")).toBeNull(); }); }); describe("with formatters", () => { let valueInput: HTMLInputElement; beforeEach(() => { const awesomeFormatter = { name: "awesome", read: (value) => "awesome " + value, } as Formatter; const totallyFormatter = { name: "totally", read: (value, prefix) => prefix + " totally " + value, } as Formatter; const placeholderFormatter = { name: "and", read: (value, affix) => value + affix, } as Formatter; riba.module.formatter.register(awesomeFormatter); riba.module.formatter.register(totallyFormatter); riba.module.formatter.register(placeholderFormatter, "and", true); riba.module.formatter.register(placeholderFormatter, "radical", true); riba.module.formatter.register(placeholderFormatter, "totally", true); }); it("register all formatters", () => { valueInput = document.createElement("input"); valueInput.setAttribute("type", "text"); valueInput.setAttribute( "data-value", "obj.name | awesome | radical | totally" ); view = riba.bind(valueInput, { obj: { name: "nothing" } }); binding = view.bindings[0] as TextBinder; expect(binding.formatters).toEqual(["awesome", "radical", "totally"]); }); it("allows arguments with pipes", () => { valueInput = document.createElement("input"); valueInput.setAttribute("type", "text"); valueInput.setAttribute( "data-value", `obj.name | awesome | totally 'arg | with || pipes' 'and more args' | and 'others formatters' with 'pi||pes'` ); view = riba.bind(valueInput, { obj: { name: "nothing" } }); binding = view.bindings[0] as TextBinder; expect(binding.formatters).toEqual([ "awesome", `totally 'arg | with || pipes' 'and more args'`, `and 'others formatters' with 'pi||pes'`, ]); }); it("cleans formatter subscriptions on unbind", () => { let cleanupCalls = 0; const cleanupFormatter = { name: "cleanup-test", read: (value: string, context: any) => { context?.addCleanup(() => { cleanupCalls++; }, "subscription"); return value; }, } as Formatter; riba.module.formatter.register(cleanupFormatter); const cleanupEl = document.createElement("div"); cleanupEl.setAttribute("data-text", "obj.name | cleanup-test"); const cleanupView = riba.bind(cleanupEl, { obj: { name: "test" } }); const cleanupBinding = cleanupView.bindings[0] as TextBinder; // Re-sync replaces the old cleanup and calls it once. cleanupBinding.sync(); expect(cleanupCalls).toBe(1); cleanupBinding._unbind(); expect(cleanupCalls).toBe(2); }); it("supports formatter-triggered invalidation without model changes", async () => { let suffix = "a"; const listeners = new Set<() => void>(); const emitter = { on(_eventName: string, callback: () => void) { listeners.add(callback); }, off(_eventName: string, callback: () => void) { listeners.delete(callback); }, emit() { listeners.forEach((listener) => listener()); }, }; const reactiveFormatter = { name: "reactive-test", read: (value: string, context: any) => { if (context) { context.on( emitter, "changed", () => context.invalidate(), "changed-subscription", ); } return `${value}-${suffix}`; }, } as Formatter; riba.module.formatter.register(reactiveFormatter); const reactiveEl = document.createElement("div"); reactiveEl.setAttribute("data-text", "obj.name | reactive-test"); const reactiveView = riba.bind(reactiveEl, { obj: { name: "test" } }); expect(reactiveEl.textContent).toBe("test-a"); suffix = "b"; emitter.emit(); await new Promise((resolve) => setTimeout(resolve, 0)); expect(reactiveEl.textContent).toBe("test-b"); reactiveView.unbind(); expect(listeners.size).toBe(0); }); }); describe("bind()", () => { it("subscribes to the model for changes via the adapter", () => { vi.spyOn(adapter, "observe"); binding._bind(); expect(adapter.observe).toHaveBeenCalledWith(model, "name", binding); }); it(`calls the binder's bind method if one exists`, () => { expect(() => { binding._bind(); }).not.toThrow(); binding.bind = () => { return; }; vi.spyOn(binding, "bind" as any); binding._bind(); expect(binding.bind).toHaveBeenCalled(); }); describe("with preloadData set to true", () => { beforeEach(() => { riba.preloadData = true; }); it("sets the initial value", () => { vi.spyOn(binding, "set"); binding._bind(); expect(binding.set).toHaveBeenCalledWith("test"); }); }); }); describe("unbind()", () => { describe("without a binder.unbind defined", () => { it("should not throw an error", () => { expect(() => { binding._unbind(); }).not.toThrow(); }); }); describe("with a binder.unbind defined", () => { beforeEach(() => { binding.unbind = () => { /**/ }; }); it("should not throw an error", () => { expect(() => { binding._unbind(); }).not.toThrow(); }); it(`calls the binder's unbind method`, () => { vi.spyOn(binding, "unbind" as any); binding._unbind(); expect(binding.unbind).toHaveBeenCalled(); }); }); }); describe("set()", () => { it("performs the binding routine with the supplied value", () => { routineFn = vi.spyOn(binding, "routine"); binding.set("sweater"); expect(routineFn).toHaveBeenCalledWith(el, "sweater"); }); it("applies any formatters to the value before performing the routine", () => { if (!view.options.formatters) { throw new Error("formatters are undefined!"); } routineFn = vi.spyOn(binding, "routine"); view.options.formatters.awesome = { name: "awesome", read(value: string) { return "awesome " + value; }, }; if (binding.formatters) { binding.formatters.push("awesome"); } binding.set("sweater"); expect(binding.routine).toHaveBeenCalledWith(el, "awesome sweater"); }); }); describe("publish()", () => { let numberInput: HTMLInputElement; it(`should publish the value of a number input`, () => { numberInput = document.createElement("input"); numberInput.setAttribute("type", "number"); numberInput.setAttribute("data-value", "obj.num"); view = riba.bind(numberInput, { obj: { num: 42 } }); binding = view.bindings[0] as TextBinder; model = binding.model; numberInput.value = "42"; vi.spyOn(adapter, "set"); binding.publish(); expect(adapter.set).toHaveBeenCalledWith(model, "num", "42"); }); }); describe("publishTwoWay()", () => { let numberInput; let valueInput; it("applies a two-way read formatter to function same as a single-way", () => { const awesomeFormatter = { read: (value) => "awesome " + value, } as Formatter; riba.module.formatter.register(awesomeFormatter, "awesome"); routineFn = vi.spyOn(binding, "routine"); if (!binding.formatters) { throw new Error("Formatters not set!"); } binding.formatters.push("awesome"); binding.set("sweater"); expect(routineFn).toHaveBeenCalledWith(el, "awesome sweater"); }); it(`should publish the value of a number input`, () => { // TODO remove any (riba.formatters.awesome as any) = { publish: (value: string) => "awesome " + value, }; numberInput = document.createElement("input"); numberInput.setAttribute("type", "number"); numberInput.setAttribute("data-value", "obj.num | awesome"); view = riba.bind(numberInput, { obj: { num: 42 } }); binding = view.bindings[0] as TextBinder; model = binding.model; numberInput.value = "42"; binding.publish(); expect(adapter.set).toHaveBeenCalledWith(model, "num", "awesome 42"); }); it(`should format a value in both directions`, () => { // TODO remove any (riba.formatters.awesome as any) = { publish: (value: string) => "awesome " + value, read: (value: string) => value + " is awesome", }; vi.spyOn(binding, "routine"); valueInput = document.createElement("input"); valueInput.setAttribute("type", "text"); valueInput.setAttribute("data-value", "obj.name | awesome"); view = riba.bind(valueInput, { obj: { name: "nothing" } }); binding = view.bindings[0] as ValueBinder; model = binding.model; vi.spyOn(binding, "routine"); valueInput.value = "charles"; binding.publish(); expect(binding.name).toEqual("value"); expect(adapter.set).toHaveBeenCalledWith( model, "name", "awesome charles" ); binding.set("fred"); expect(binding.routine).toHaveBeenCalledWith( valueInput, "fred is awesome" ); }); it(`should resolve formatter arguments to their values`, () => { (riba.formatters.withArguments as Formatter) = { name: "withArguments", publish: (value, arg1, arg2) => { return value + ":" + arg1 + ":" + arg2; }, read: (value, arg1, arg2) => { return value.replace(":" + arg1 + ":" + arg2, ""); }, }; valueInput = document.createElement("input"); valueInput.setAttribute("type", "text"); valueInput.setAttribute( "data-value", `obj.name | withArguments config.age 'male'` ); view = riba.bind(valueInput, { obj: { name: "nothing", }, config: { age: 50, }, }); binding = view.bindings[0] as ValueBinder; vi.spyOn(binding, "routine"); model = binding.model; expect(binding.name).toEqual("value"); valueInput.value = "bobby"; binding.publish(); expect(adapter.set).toHaveBeenCalledWith(model, "name", "bobby:50:male"); expect(valueInput.value).toEqual("bobby"); binding.set("andy:50:male"); expect(binding.routine).toHaveBeenCalledWith(valueInput, "andy"); }); it(`should resolve formatter arguments correctly with multiple formatters`, () => { (riba.formatters.wrap as Formatter) = { name: "wrap", publish: (value: string, arg1: string, arg2: string) => { return arg1 + value + arg2; }, read: (value: string, arg1: string, arg2: string) => { return value.replace(arg1, "").replace(arg2, ""); }, }; (riba.formatters.saveAsCase as Formatter) = { name: "saveAsCase", publish: (value, typeCase) => { return value["to" + typeCase + "Case"](); }, read: (value, typeCase) => { return value[typeCase === "Upper" ? "toLowerCase" : "toUpperCase"](); }, }; valueInput = document.createElement("input"); valueInput.setAttribute("type", "text"); valueInput.setAttribute( "data-value", `obj.name | saveAsCase config.typeCase | wrap config.curly '}' | wrap config.square ']' | wrap config.paren ')'` ); view = riba.bind(valueInput, { obj: { name: "nothing", }, config: { paren: "(", square: "[", curly: "{", typeCase: "Upper", }, }); binding = view.bindings[0] as TextBinder; model = binding.model; vi.spyOn(binding, "routine"); valueInput.value = "bobby"; binding.publish(); expect(adapter.set).toHaveBeenCalledWith(model, "name", "{[(BOBBY)]}"); expect(valueInput.value).toEqual("bobby"); binding.set("{[(ANDY)]}"); expect(binding.routine).toHaveBeenCalledWith(valueInput, "andy"); }); it(`should not fail or format if the specified binding function doesn't exist`, () => { (riba.formatters.awesome as any) = {}; valueInput = document.createElement("input"); valueInput.setAttribute("type", "text"); valueInput.setAttribute("data-value", "obj.name | awesome"); view = riba.bind(valueInput, { obj: { name: "nothing" } }); binding = view.bindings[0] as TextBinder; model = binding.model; vi.spyOn(binding, "routine"); valueInput.value = "charles"; binding.publish(); expect(adapter.set).toHaveBeenCalledWith(model, "name", "charles"); binding.set("fred"); expect(binding.routine).toHaveBeenCalledWith(valueInput, "fred"); }); it(`should apply read binders left to right, and write binders right to left`, () => { (riba.formatters.totally as Formatter) = { name: "totally", publish: (value) => value + " totally", read: (value) => value + " totally", }; (riba.formatters.awesome as Formatter) = { name: "awesome", publish: (value) => value + " is awesome", read: (value) => value + " is awesome", }; valueInput = document.createElement("input"); valueInput.setAttribute("type", "text"); valueInput.setAttribute("data-value", "obj.name | awesome | totally"); view = riba.bind(valueInput, { obj: { name: "nothing" } }); binding = view.bindings[0] as TextBinder; model = binding.model; vi.spyOn(binding, "routine"); binding.set("fred"); expect(binding.routine).toHaveBeenCalledWith( valueInput, "fred is awesome totally" ); valueInput.value = "fred"; binding.publish(); expect(adapter.set).toHaveBeenCalledWith( model, "name", "fred totally is awesome" ); }); it(`binders in a chain should be skipped if they're not there`, () => { (riba.formatters.totally as Formatter) = { name: "totally", publish: (value) => value + " totally", read: (value) => value + " totally", }; (riba.formatters.radical as Formatter) = { name: "radical", publish: (value) => value + " is radical", }; (riba.formatters.awesome as Formatter) = { name: "awesome", read: (value) => value + " is awesome", }; valueInput = document.createElement("input"); valueInput.setAttribute("type", "text"); valueInput.setAttribute( "data-value", "obj.name | awesome | radical | totally" ); view = riba.bind(valueInput, { obj: { name: "nothing" } }); binding = view.bindings[0] as ValueBinder; vi.spyOn(binding, "routine"); expect(binding.name).toEqual("value"); model = binding.model; // TODO fixme // expect(binding.routine).toHaveBeenCalledWith( // valueInput, // "nothing is awesome totally" // ); binding.set("fred"); expect(binding.routine).toHaveBeenCalledWith( valueInput, "fred is awesome totally" ); valueInput.value = "fred"; binding.publish(); expect(adapter.set).toHaveBeenCalledWith( model, "name", "fred totally is radical" ); }); }); describe("formattedValue()", () => { it("applies the current formatters on the supplied value", () => { if (!view.options.formatters) { throw new Error("formatters are undefined!"); } view.options.formatters.awesome = { name: "awesome", read(value: string) { return "awesome " + value; }, }; if (!binding.formatters) { throw new Error("Formatters not set!"); } binding.formatters.push("awesome"); expect(binding.formattedValue("hat")).toEqual("awesome hat"); }); describe("with a multi-argument formatter string", () => { beforeEach(() => { if (!view.options.formatters) { throw new Error("formatters are undefined!"); } view.options.formatters.awesome = { name: "awesome", read(value: string, prefix: string) { return prefix + " awesome " + value; }, }; if (!binding.formatters) { throw new Error("Formatters not set!"); } binding.formatters.push(`awesome 'super'`); }); it("applies the formatter with arguments", () => { expect(binding.formattedValue("jacket")).toEqual( "super awesome jacket" ); }); }); describe("with a formatter string with pipes in argument", () => { beforeEach(() => { if (!view.options.formatters) { throw new Error("formatters are undefined!"); } view.options.formatters.totally = { name: "totally", read(value: string, prefix: string) { return prefix + " totally " + value; }, }; if (!binding.formatters) { throw new Error("Formatters not set!"); } binding.formatters.push(`totally 'arg | with || pipes'`); }); it("applies the formatter with arguments with pipes", () => { expect(binding.formattedValue("jacket")).toEqual( "arg | with || pipes totally jacket" ); }); }); }); describe("getValue()", () => { it("should use binder.getValue() if present", () => { binding.getValue = () => { return "foo"; }; expect(binding._getValue(el)).toEqual("foo"); }); it("binder.getValue() should have access to passed element", () => { binding.getValue = (_el: HTMLElement) => { return _el.dataset.foo; }; el.dataset.foo = "bar"; expect(binding._getValue(el)).toEqual("bar"); }); it("binder.getValue() should have access to binding", () => { binding.getValue = function () { return (this as any).foo; }; (binding as any).foo = "bar"; expect(binding._getValue(el)).toEqual("bar"); }); }); }); describe("Functional", () => { const riba = new Riba(); riba.module.adapter.register(dotAdapter); riba.module.binder.register(TextBinder); riba.module.binder.register(HtmlBinder); riba.module.binder.register(ValueBinder); riba.module.binder.register(EachStarBinder); riba.module.binder.register(AddClassBinder); let data: Data; let bindData: { data: Data }; let el: HTMLUnknownElement; let input: HTMLInputElement; let originalPrefix: string[]; let adapter: Adapter; beforeEach(() => { originalPrefix = riba.prefix; riba.prefix = ["data"]; adapter = { name: ":", observe: (obj, keypath, callback) => { obj.on(keypath, callback); }, unobserve: (obj, keypath, callback) => { obj.off(keypath, callback); }, get: (obj, keypath) => { return obj.get(keypath); }, set: (obj, keypath, value) => { const attributes: { [keypath: string]: any } = {}; attributes[keypath] = value; obj.set(attributes); }, }; riba.module.adapter.register(adapter); riba.configure({ preloadData: true }); data = new Data({ foo: "bar", items: [{ name: "a" }, { name: "b" }], }); bindData = { data }; el = document.createElement("div"); el.setAttribute("rv-value", ""); input = document.createElement("input"); input.setAttribute("type", "text"); }); afterEach(() => { riba.prefix = originalPrefix; }); describe("Binds", () => { describe("Text", () => { it("should set the text content of the element", () => { el.setAttribute("data-text", "data:foo"); riba.bind(el, bindData); expect(el.textContent).toEqual(data.get("foo")); }); it("should correctly handle HTML in the content", () => { el.setAttribute("data-text", "data:foo"); const value = "<b>Fail</b>"; data.set({ foo: value }); riba.bind(el, bindData); expect(el.textContent).toEqual(value); }); }); describe("HTML", () => { it("should set the html content of the element", () => { el.setAttribute("data-html", "data:foo"); riba.bind(el, bindData); expect(el.textContent).toEqual(data.get("foo")); }); it("should correctly handle HTML in the content", () => { el.setAttribute("data-html", "data:foo"); const value = "<b>Fail</b>"; data.set({ foo: value }); riba.bind(el, bindData); expect(el.innerHTML).toEqual(value); }); }); describe("Value", () => { it("should set the value of the element", () => { input.setAttribute("data-value", "data:foo"); riba.bind(input, bindData); expect(input.value).toEqual(data.get("foo")); }); }); describe("Multiple", () => { it("should bind a list of multiple elements", () => { el.setAttribute("data-html", "data:foo"); input.setAttribute("data-value", "data:foo"); riba.bind([el, input], bindData); expect(el.textContent).toEqual(data.get("foo")); expect(input.value).toEqual(data.get("foo")); }); }); describe("Priority", () => { let mockA: any; // TODO: vi.Mock<any, any>; let mockB: any; // TODO: vi.Mock<any, any>; let mockC: any; // TODO: vi.Mock<any, any>; let mockD: any; // TODO: vi.Mock<any, any>; let mockE: any; // TODO: vi.Mock<any, any>; let mockF: any; // TODO: vi.Mock<any, any>; let mockG: any; // TODO: vi.Mock<any, any>; beforeEach(() => { mockA = vi.fn(); mockB = vi.fn(); mockC = vi.fn(); mockD = vi.fn(); mockE = vi.fn(); mockF = vi.fn(); mockG = vi.fn(); class ABinder extends Binder<any, any> { static key = "a"; bind = mockA; routine() { /**/ } } class BBinder extends Binder<any, any> { static key = "b"; bind = mockB; routine() { /**/ } } class CBinder extends Binder<any, any> { static key = "c"; priority = 10; bind = mockC; routine() { /**/ } } class DBinder extends Binder<any, any> { static key = "d"; priority = 30; bind = mockD; routine() { /**/ } } class EBinder extends Binder<any, any> { static key = "e"; priority = 5; bind = mockE; routine() { /**/ } } class FBinder extends Binder<any, any> { static key = "f"; priority = 2; bind = mockF; routine() { /**/ } } class GBinder extends Binder<any, any> { static key = "g"; priority = 1; bind = mockG; routine() { /**/ } } riba.module.binder.register(ABinder); riba.module.binder.register(BBinder); riba.module.binder.register(CBinder); riba.module.binder.register(DBinder); riba.module.binder.register(EBinder); riba.module.binder.register(FBinder); riba.module.binder.register(GBinder); el.setAttribute("data-a", "data:foo"); el.setAttribute("data-b", "data:foo"); el.setAttribute("data-c", "data:foo"); el.setAttribute("data-d", "data:foo"); el.setAttribute("data-e", "data:foo"); el.setAttribute("data-f", "data:foo"); el.setAttribute("data-g", "data:foo"); }); describe("a:10, b:30", () => { beforeEach(() => { const view = riba.bind(el, bindData); const c = view.bindings[1]; const d = view.bindings[0]; expect(c.name).toEqual("c"); expect(c.priority).toEqual(10); expect(d.name).toEqual("d"); expect(d.priority).toEqual(30); }); it("should bind d before c", () => { expect(mockD).toHaveBeenCalledBefore(mockC); }); }); describe("a:5, b:2", () => { beforeEach(() => { const view = riba.bind(el, bindData); const e = view.bindings[2]; const f = view.bindings[3]; expect(e.name).toEqual("e"); expect(e.priority).toEqual(5); expect(f.name).toEqual("f"); expect(f.priority).toEqual(2); }); it("should bind e before f", () => { expect(mockE).toHaveBeenCalledBefore(mockF); }); }); describe("a:undefined, g:1", () => { beforeEach(() => { const view = riba.bind(el, bindData); const g = view.bindings[4]; expect(g.name).toEqual("g"); expect(g.priority).toEqual(1); riba.bind(el, bindData); }); it("should bind g before a", () => { expect(mockG).toHaveBeenCalledBefore(mockA); }); }); }); describe("Iteration", () => { let listItem: HTMLLIElement; let list: HTMLUListElement; beforeEach(() => { list = document.createElement("ul"); el.appendChild(list); listItem = document.createElement("li"); listItem.setAttribute("data-each-item", "data:items"); list.appendChild(listItem); }); it("should loop over a collection and create new instances of that element + children", () => { expect(el.getElementsByTagName("li").length).toEqual(1); riba.bind(el, bindData); expect(el.getElementsByTagName("li").length).toEqual(2); }); it("should not fail if the collection being bound to is null", () => { data.set({ items: null }); riba.bind(el, bindData); expect(el.getElementsByTagName("li").length).toEqual(0); }); it("should re-loop over the collection and create new instances when the array changes", () => { riba.bind(el, bindData); expect(el.getElementsByTagName("li").length).toEqual(2); const newItems = [{ name: "a" }, { name: "b" }, { name: "c" }]; data.set({ items: newItems }); expect(el.getElementsByTagName("li").length).toEqual(3); }); it("should allow binding to the iterated item as well as any parent contexts", () => { const span1 = document.createElement("span"); span1.setAttribute("data-text", "item.name"); const span2 = document.createElement("span"); span2.setAttribute("data-text", "data:foo"); listItem.appendChild(span1); listItem.appendChild(span2); riba.bind(el, bindData); expect(el.getElementsByTagName("span")[0].textContent).toEqual("a"); expect(el.getElementsByTagName("span")[1].textContent).toEqual("bar"); }); it("should allow binding to the iterated element directly", () => { listItem.setAttribute("data-text", "item.name"); listItem.setAttribute("data-add-class", "data:foo"); riba.bind(el, bindData); expect(el.getElementsByTagName("li")[0].textContent).toEqual("a"); expect(el.getElementsByTagName("li")[0].className).toEqual("bar"); }); it("should insert items between any surrounding elements", () => { const firstItem = document.createElement("li"); const lastItem = document.createElement("li"); firstItem.textContent = "first"; lastItem.textContent = "last"; list.appendChild(lastItem); list.insertBefore(firstItem, listItem); listItem.setAttribute("data-text", "item.name"); riba.bind(el, bindData); expect(el.getElementsByTagName("li")[0].textContent).toEqual("first"); expect(el.getElementsByTagName("li")[1].textContent).toEqual("a"); expect(el.getElementsByTagName("li")[2].textContent).toEqual("b"); expect(el.getElementsByTagName("li")[3].textContent).toEqual("last"); }); // TODO fixme it.skip("should allow binding to the iterated element index", () => { listItem.setAttribute("data-index", "%item%"); riba.bind(el, bindData); expect(el.getElementsByTagName("li")[0].getAttribute("index")).toEqual( "0" ); expect(el.getElementsByTagName("li")[1].getAttribute("index")).toEqual( "1" ); }); // TODO fixme it.skip("should allow the developer to configure the index attribute available in the iteration", () => { listItem.setAttribute("data-index", "itemIndex"); listItem.setAttribute("index-property", "itemIndex"); riba.bind(el, bindData); expect(el.outerHTML).toEqual(""); // debug expect(el.getElementsByTagName("li")[0].getAttribute("index")).toEqual( "0" ); expect(el.getElementsByTagName("li")[1].getAttribute("index")).toEqual( "1" ); }); }); }); describe("Updates", () => { it("should change the value", () => { el.setAttribute("data-text", "data:foo"); riba.bind(el, bindData); data.set({ foo: "some new value" }); expect(el.textContent).toEqual(data.get("foo")); }); }); describe("Input", () => { it("should update the model value", () => { input.setAttribute("data-value", "data:foo"); riba.bind(input, bindData); input.value = "some new value"; const event = document.createEvent("HTMLEvents"); event.initEvent("input", true, true); input.dispatchEvent(event); expect(data.get("foo")).toEqual("some new value"); }); it("should allow to change the event listened", () => { let event; input.setAttribute("data-value", "data:foo"); input.setAttribute("event-name", "blur"); riba.bind(input, bindData); input.value = "some new value"; event = document.createEvent("HTMLEvents"); event.initEvent("input", true, true); input.dispatchEvent(event); expect(data.get("foo")).toEqual("bar"); event = document.createEvent("HTMLEvents"); event.initEvent("blur", true, true); input.dispatchEvent(event); expect(data.get("foo")).toEqual("some new value"); }); }); });