Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
491 changes: 428 additions & 63 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
packages:
- 'ember-primitives'
- 'primitives'
- 'test-app'
- 'docs-app'
- 'dev'
24 changes: 24 additions & 0 deletions primitives/babel.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module.exports = {
plugins: [
[
'@babel/plugin-transform-typescript',
{
allExtensions: true,
onlyRemoveTypeImports: true,
allowDeclareFields: true,
},
],
[
'module:decorator-transforms',
{
runtime: {
import: require.resolve('decorator-transforms/runtime-esm'),
},
},
],
],

generatorOpts: {
compact: false,
},
};
29 changes: 29 additions & 0 deletions primitives/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@nullvoxpopuli/primitives",
"type": "module",
"dependencies": {
"decorator-transforms": "^2.2.2",
"@lit/context": "^1.1.3",
"lit": "^3.2.1"
},
"files": [
"src"
],
"exports": {
".": {
"default": "./src/index.js"
},
"./*": {
"default": "./src/*"
}
},
"devDependencies": {
"@babel/core": "^7.25.8",
"@babel/plugin-transform-typescript": "^7.25.7",
"@rollup/plugin-babel": "^6.0.4",
"@tsconfig/ember": "^3.0.5",
"prettier": "^3.2.5",
"typescript": "^5.6.3",
"vite": "^5.4.9"
}
}
Empty file added primitives/src/index.ts
Empty file.
84 changes: 84 additions & 0 deletions primitives/src/switch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { html, LitElement } from 'lit';
import { createContext, consume, provide } from '@lit/context';
import { property } from 'lit/decorators.js';

let i = 0;
function uniqueId() {
i++;
return `primitiveId-${i}`;
}

const stateContext = createContext('switch-state');
/**
* emits "change" event
*/
class Switch extends LitElement {
static properties = {
checked: { type: Boolean },
};

@provide({ context: stateContext }) id = uniqueId();

handleChange = (event: Event) => {
this.dispatchEvent(
new CustomEvent('change', {
detail: {
checked: event.target.checked,
},
bubbles: true,
})
);
};

render() {
return html`<div></div>`;
}
}

class Control extends HTMLElement {
@consume({ context: stateContext })
@property({ attribute: false })
declare id: string;

constructor() {
super();

console.log('id', this.id);
this.setAttribute('id', this.id);
this.innerHTML = `<input id=${this.id}>`;
}

render() {
return html`<input id=${this.id} />`;
}
}

class Label extends HTMLElement {
@consume({ context: stateContext })
@property({ attribute: false })
declare id: string;

constructor() {
super();

console.log('id', this.id);
this.setAttribute('for', this.id);
this.innerHTML = `<label for=${this.id}></label>`;
}
// render() {
// return html`<label for=${this.id}></label>`;
// }
}

Switch.Control = Control;
Switch.Label = Label;

customElements.define('primitive-switch', Switch);
customElements.define('primitive-switch-control', Switch.Control);
// either:
// <primitive-switch-label>
// or
// <label is="primitive-switch-label">
customElements.define('primitive-switch-label', Switch.Label, {
extends: 'label',
});
44 changes: 44 additions & 0 deletions primitives/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"extends": "@tsconfig/ember/tsconfig.json",
"include": ["src/**/*", "unpublished-development-types/**/*"],
"compilerOptions": {
"skipLibCheck": true,
"declarationDir": "declarations",
"emitDeclarationOnly": true,

// Required, else declarations don't emit
// @tsconfig/ember sets noEmit: true
"noEmit": false,
// noEmitOnError true is not a good default.
// especially as CLI tools can accidentally swallow errors
"noEmitOnError": false,

/**
https://www.typescriptlang.org/tsconfig#rootDir
"Default: The longest common path of all non-declaration input files."

Because we want our declarations' structure to match our rollup output,
we need this "rootDir" to match the "srcDir" in the rollup.config.mjs.

This way, we can have simpler `package.json#exports` that matches
imports to files on disk
*/
"rootDir": "./src",

/**
https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax

We don't want to include types dependencies in our compiled output, so tell TypeScript
to enforce using `import type` instead of `import` for Types.
*/
"verbatimModuleSyntax": true,

/**
https://www.typescriptlang.org/tsconfig#allowImportingTsExtensions

We want our tooling to know how to resolve our custom files so the appropriate plugins
can do the proper transformations on those files.
*/
"allowImportingTsExtensions": true
}
}
44 changes: 44 additions & 0 deletions primitives/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { readFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';

import { babel } from '@rollup/plugin-babel';
import { defineConfig } from 'vite';

const entrypoints = ['index.ts', 'switch.ts'];

const manifestStr = await readFile(join(import.meta.dirname, 'package.json'));
const manifest = JSON.parse(manifestStr);
// Why is this not default?
// Why else would you specify (peer)deps?
const externals = [
...Object.keys(manifest.dependencies ?? {}),
...Object.keys(manifest.peerDependencies ?? {}),
];

export default defineConfig({
build: {
outDir: 'dist',
// These targets are not "support".
// A consuming app or library should compile further if they need to support
// old browsers.
target: ['esnext', 'firefox121'],
minify: false,
sourcemap: true,
lib: {
// Could also be a dictionary or array of multiple entry points
entry: entrypoints.map((name) => resolve(import.meta.dirname, 'src', name)),
name: 'primitives',
formats: ['es'],
// the proper extensions will be added
},
rollupOptions: {
external: [...externals, 'lit/decorators.js'],
},
},
plugins: [
babel({
babelHelpers: 'bundled',
extensions: ['.js', '.ts'],
}),
],
});
4 changes: 2 additions & 2 deletions test-app/ember-cli-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module.exports = async function (defaults) {
app: (() => {
let sideWatch = require('@embroider/broccoli-side-watch');

let paths = ['ember-primitives'].map((libraryName) => {
let paths = ['ember-primitives', '@nullvoxpopuli/primitives'].map((libraryName) => {
let entry = require.resolve(libraryName);
let { packageJson, path: packageJsonPath } = readPackageUpSync({ cwd: entry });
let packagePath = path.dirname(packageJsonPath);
Expand All @@ -27,7 +27,7 @@ module.exports = async function (defaults) {
if (!fs.existsSync(p)) return false;
if (!fs.lstatSync(p).isDirectory()) return false;

return !p.endsWith('/src');
return true;
});

return toWatch;
Expand Down
1 change: 1 addition & 0 deletions test-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"_syncPnpm": "DEBUG=sync-pnpm pnpm sync-dependencies-meta-injected"
},
"dependencies": {
"@nullvoxpopuli/primitives": "workspace:*",
"ember-primitives": "workspace:*",
"ember-resources": "^7.0.0",
"ember-route-template": "^1.0.3",
Expand Down
30 changes: 30 additions & 0 deletions test-app/tests/primitives/wip-test.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import '@nullvoxpopuli/primitives/switch';

import { click, render } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';


module('Rendering | <primitive-switch>', function (hooks) {
setupRenderingTest(hooks);

test('it works', async function (assert) {
const handleChange = (value: unknown) => assert.step(`Change:${value}`);

await render(
<template>
<primitive-switch>
<primitive-switch-label>

</primitive-switch-label>
<primitive-switch-control>

</primitive-switch-control>
</primitive-switch>
</template>
);

await this.pauseTest();
assert.dom('label').exists();
});
});