Coder Social home page Coder Social logo

matthewp / corset Goto Github PK

View Code? Open in Web Editor NEW
274.0 4.0 2.0 667 KB

Declarative data bindings, bring your own backend.

Home Page: https://corset.dev/

License: BSD 2-Clause "Simplified" License

Makefile 0.15% JavaScript 86.27% HTML 0.59% C 10.56% TypeScript 2.43%
ui reactive framework

corset's Introduction

Corset CI workflow

Getting Started

Please follow the documentation at corset.dev to learn how to use the library.

Supporting Corset

Corset is an open-source (BSD licensed) project. If you'd like to support our efforts, get in touch with me at [email protected].

License

BSD-2-Clause

Copyright (c) 2022-present, Matthew Phillips

corset's People

Contributors

matthewp avatar github-actions[bot] avatar

Stargazers

Alexandre Bonaventure Geissmann avatar Dante avatar Kirtis avatar Andrii Matenka avatar Tobz avatar  avatar Alexey Zaharchenko avatar Alexandre Stahmer avatar Tuan Duc Tran avatar Paul Valladares avatar Alfin S. avatar benli avatar Tim Kersey avatar Jaden Geller avatar Zhijun avatar Simon O avatar Nils Kjellman avatar Hirohisa Kun avatar Kayla Firestack avatar Ian Luca avatar Konstantin BIFERT avatar Dineshkumar Ponnusamy avatar ajith avatar  avatar  avatar Michael Wheeler avatar Nikhil Reddy avatar Geoffrey Mureithi avatar Richard avatar Jannis Morgenstern avatar Nurul Akbar al-Ghifari avatar Boticello avatar Ara L Abcarians avatar blurk avatar Michael McClintock avatar ⊣˚∆˚⊢ avatar  avatar Jan Fooken avatar Thales Menezes avatar Les Pruszynski avatar Alex Hays avatar Marcelle Rusu avatar  avatar Amrita Chanda avatar Tyler Brostrom avatar Chris Weekly avatar Ziad El Khoury Hanna avatar Nguyen Vu Cuong (Ralph) avatar Lloyd Kupchanko avatar Ivan Malopinsky avatar Justin Noel avatar Jan avatar Enoch avatar Derrick Farris avatar  avatar  avatar  avatar Fudzer M Huda avatar LE MONIES DE SAGAZAN Mayeul avatar Mathieu Laurent avatar Darren Shewry avatar Juan P. Prieto avatar Steve Sewell avatar Mr.Q avatar Juho Vepsäläinen avatar Nyi Nyi Lwin avatar Ted Klingenberg avatar frankfanslc avatar Beeno Tung avatar  avatar Reggi avatar Porkopek avatar Steven Yung avatar Nathan avatar Saiful Islam avatar Isaac Weber avatar Damir Perisic avatar Vitalii Kiiko avatar Dom Lazic avatar Aditia Pamungkas avatar Adrien Zinger avatar Randall Leeds avatar Joe Pea avatar Priestch avatar Dino avatar Andrew Young avatar Ryan Foote avatar Alex Lyalin avatar Rohan Rajpal avatar Alexandros Papadopoulos avatar  avatar  avatar Dmytro Pushkarchuk avatar Yaroslav Lapin avatar Michael Raguse avatar Mehdi avatar  avatar Алекс Рассудихин avatar Neo Lee avatar Álvaro García León avatar

Watchers

Ivan Malopinsky avatar  avatar James Cloos avatar  avatar

corset's Issues

Allow multi-binding properties on the left side

Suggestion from @propjockey; managing multi-binding properties is a bit cumbersome in CSS and in Corset. In Corset we try to allow a selector to override only the bindings that it sets and not all of them. There are likely bugs here though.

What if we changed it so that the type (the first value) was part of the property instead of in the value. So for example:

.profile {
  class-toggle: admin var(--is-admin);
}

Becomes

.profile {
  class-toggle[admin]: var(--is-admin);
}

Another example for events:

.login-form {
  event[submit]: var(--handler);
}

One use-case we want to preserve from the current array form is to allow dynamic types. For example you can do:

#app {
  class-toggle: var(--mode) var(--mode-enabled);
}

The above will allow you to change the class name being toggled by setting --mode. An example where you might do this: if you have light/dark mode and toggle them by setting a class name either .dark-mode or .light-mode. Using --mode allows you to choose that class name dynamically rather than making either light or dark the default.

With the left-side approach we could still allow this dynamism like so:

#app {
  class-toggle[--mode]: var(--mode-enabled);
}

The presence of -- in the type would signal that it is a var; normal var lookup rules would apply.

Another use-case for using --prop is if the type is not a string. For class-toggle, attr, attr-toggle, data, and prop they are always strings. For events there's a chance that Symbols might eventually be allowed as event names. There wouldn't be a way to set the event name in the short syntax if only identifiers were allowed.

We might also allow for JS insertions here, I could see:

#app {
  event[${Symbol.for('some-event')}]: var(--handler);
}

Although I don't know how we would know when one overrides the other, so I would probably put off allowing this for now.


One exciting thing about potentially doing this, is it would possibly solve #52, as identifiers-as-strings are mostly necessary for the "type" of a multi-binding property. Moving to this method would take away that need. Freeing us up to implement special identifiers like initial.

Selector Bug

copying over from the twitter DMs and adding more info ~
If I have this Test Corset component (A):

import sheet from "https://cdn.corset.dev/0.8.10/main.js"

export const Template = (t => (t.innerHTML = `
  <h1>Hello World</h1>
  Count is <span class="count"></span>
  
  <button type="button" class="increment">Increment</button>
`, t))(document.createElement("template"))

export const Behavior = class {
  static inputProperties = ["--initial-count"]

  constructor (props) {
    this.count = props.get("--initial-count")
  }

  increment () {
    this.count++
  }

  bind () {
    const { count } = this

    return sheet`
      .count {
        --count: ${count};
        text: var(--count);
      }

      .increment {
        --cb: ${this.increment};
        event: click var(--cb);
      }
    `
  }
}

and this higher level App component that consumes it (B):

import sheet from "https://cdn.corset.dev/0.8.10/main.js"
import * as Test from "./components/test.js"

export const Template = (t => (t.innerHTML = `
  <div></div>
  <div class="shared counter-1"></div>
  <div class="shared counter-2"></div>
`, t))(document.createElement("template"))

export const Behavior = class {
  bind () {
    return sheet`
      .counter-1,
      .counter-2 {
        --initial-count: ${22};
        attach-template: ${Test.Template};
        behavior: mount(${Test.Behavior});
      }
    `
  }
}
And this is index.html (C)
<!doctype html>
<html>
  <head>
  </head>
  <body>
    <div id="app"></div>

    <script type="module">
      import sheet, { mount } from "https://cdn.corset.dev/0.8.10/main.js"
      import * as App from "./src/app.js"
      mount(document.body, class {
        bind () {
          return sheet`
             #app {
              attach-template: ${App.Template};
              behavior: mount(${App.Behavior});
            }
          `
        }
      })
    </script>
  </body>
</html>

I get this output with a single working counter:
image

If I change the selector in my sheet from (B) to either of these variants below, both counters will be populated and working:

.counter-1,
.counter-2,
.FOOBAR { ... }
.shared { ... }

image

I believe it's in part because of the hyphen in the selector because .counter-1 { ... } by itself also does not work even though .shared does

Happy to poke the regex myself and suggest the fix if you can to point me to it!

Child to parent bindings

Discussed this elsewhere, but custom functions could be made to work with pretty easily. I could see something like this:

registerCustomFunction('--resolve', class {
  constructor() {
    this.state = 'pending';
    this.value = null;
    this.reason = null;
  }
  check([val], { rebind }) {
    Promise.resolve(val).then(resolvedValue => {
      this.value = resolvedValue;
      this.state = 'resolved';
    }).catch(err => {
      this.state = 'rejected';
      this.reason = err;
    });
  }
  call([]) {
    return { value: this.value, state: this.state, reason: this.reason };
  }
});

Which you could then use like this:

sheet`
  .page {
    --promise: --resolve(${Promise.resolve(1)});
    --state: get(var(--promise), state);
    class-toggle[--state]: true;
  }

  .page.pending {
    text: "Waiting";
  }

  .page.rejected {
    --reason: get(var(--promise), reason);
    text: "Oops, there was a problem: " var(--reason);
  }

  .page.resolved {
    --value: get(var(--promise), value);
    text: "We got: " var(--value);
  }
`;

Ability to target the root element

Currently you can't target the root element. This means you wind up using wrappers more than you'd like. One area where this is frustrating is adding a behavior to a specific element that might not even have children.

I think the fix would be a custom selector, maybe something like :corset-root that would allow targeting the root:

:corset-root {
  class-toggle[testing]: true;
}

To add this feature we first need to parse selectors, which doesn't happen at the moment.

Support unordered shorthand

In CSS shorthand can be unordered: https://developer.mozilla.org/en-US/docs/Web/CSS/Shorthand_properties#ordering_properties

In Corset ordering is strictly enforced. This makes it harder to use properties like event which has a very long signature. It would be nice if you could do:

body {
  event: var(--target) onpopstate var(--callback);
}

I think the algorithm would go something like this:

  1. Check if the value is of the right type
  • Probably the type would be part of the longhand prop declaration.
  1. If not, start at the first unfulfilled property until you find one of the right type.

Add number types

Probably integer and float types but need to research what CSS's are.

invalid var reference is using a different var that almost matches

Reduced example:
https://codepen.io/propjockey/pen/poLppJX/2c1726852bc96c8bf7703c95486a97c5?editors=1010
The each-template prop uses a var that is referencing --bad-match which doesn't exist, but is substituted with the value on var called --bad-match2.

If --bad-match2 ends with anything other than digits, the collision bug does not occur. Ex:
--bad-match22 found by var(--bad-match)
--bad-matcha (correctly) NOT found by var(--bad-match)
--bad-match-2 (correctly) NOT found by var(--bad-match)

probably related to #155

Ty! 💜

Performance

These are the levers to pull:

  • querySelectorAll
    • Currently this happens on every update. How can we limit this to only when something has changed? Things that can invalidate qSA:
      • attr
      • class-toggle
      • each
      • attach-template
  • Dirty check to see if values have changed.
    • Insertions need to be checked every time.
    • var() needs to be checked when a dependent query changes.
  • matches
    • Similar to the qSA problem, checked on every render. Should skip if there isn't an invalidation.

Change the mount property to behavior and allow multiple mounts

I think this is a better API

class Counter {
  static inputProperties = ['--start-count'];
  bind(props) {
    let count = this.state.count || props.get('count');

    return sheet`
      .count {
        text: ${count};
      }
    `;
  }
}

sheet`
  #app {
    behavior: mount(${Counter}), mount(${SomethingElse});
  }
`

This will allow:

  • Multiple mounts on the same element for mixin like behavior.
  • Other types of behaviors (maybe lower-level than a mount).

New mount api

Just something I've been thinking about:

import sheet, { mount } from 'https://cdn.corset.dev/0.4.0/main.js';

mount(document, class {
  constructor() {
    this.count = 0;
  }

  bind() {
    let { count } = this;
    return sheet`
      .counter {
        text: ${count};
      }
    `;
  }
});

More verbose but also simpler. TBD

Unable to build with vite and ts

At the moment it is not possible to build for production using vite and a local installed version of corset via npm. The issue is happening during typecheck with tsc.

Steps to reproduce:

current result:

❯ npm run build
$ tsc && vite build
node_modules/corset/lib/binding.d.ts:75:53 - error TS2694: Namespace '"/home/projects/vitejs-vite-qczl4g/node_modules/corset/lib/property"' has no exported member 'PropertyPropName'.

75 export type PropertyPropName = import('./property').PropertyPropName;
                                                       ~~~~~~~~~~~~~~~~

node_modules/corset/lib/property.d.ts:26:13 - error TS2456: Type alias 'PropertyDefinition' circularly references itself.

26 export type PropertyDefinition = import('./property').PropertyDefinition;
               ~~~~~~~~~~~~~~~~~~

node_modules/corset/lib/property.d.ts:27:13 - error TS2456: Type alias 'SimplePropertyDefinition' circularly references itself.

27 export type SimplePropertyDefinition = import('./property').SimplePropertyDefinition;
               ~~~~~~~~~~~~~~~~~~~~~~~~

node_modules/corset/lib/property.d.ts:28:13 - error TS2456: Type alias 'ShorthandPropertyDefinition' circularly references itself.

28 export type ShorthandPropertyDefinition = import('./property').ShorthandPropertyDefinition;
               ~~~~~~~~~~~~~~~~~~~~~~~~~~~

node_modules/corset/lib/property.d.ts:29:13 - error TS2456: Type alias 'LonghandPropertyDefinition' circularly references itself.

29 export type LonghandPropertyDefinition = import('./property').LonghandPropertyDefinition;
               ~~~~~~~~~~~~~~~~~~~~~~~~~~

node_modules/corset/lib/property.d.ts:30:13 - error TS2456: Type alias 'KeyedMultiPropertyDefinition' circularly references itself.

30 export type KeyedMultiPropertyDefinition = import('./property').KeyedMultiPropertyDefinition;
               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~

node_modules/corset/lib/property.d.ts:31:13 - error TS2456: Type alias 'BehaviorMultiPropertyDefinition' circularly references itself.

31 export type BehaviorMultiPropertyDefinition = import('./property').BehaviorMultiPropertyDefinition;
               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

src/main.ts:9:12 - error TS7006: Parameter 'e' implicitly has an 'any' type.

9     toggle(e) {
             ~


Found 8 errors in 3 files.

Errors  Files
     1  node_modules/corset/lib/binding.d.ts:75
     6  node_modules/corset/lib/property.d.ts:26
     1  src/main.ts:9

~/projects/vitejs-vite-qczl4g

var not being recognized

Corset v1 and v2

in the following Corset sheet code, var(--onSelected, false) is resolving as false:

      [role="group"] {
        --onSelected: ${x => console.log(x)};
        event: pointerdown bind(
          ${this.someEvent},
          var(--onSelected, false)
        );
        event: keydown bind(
          ${this.someEvent},
          var(--onSelected, false)
        );
      }
  someEvent (selCB, ev) {
    // ... do stuff
    selCB && selCB(1) // selCB is false :(
  }

Changing the variable to --on-selected resolves errors - might have a toLowerCase() happening(?) but vars are case sensitive

Ty! 💜

Needed feature - A way to bind global (document, window, etc.) event listeners to the sheet.

I feel as if there are a lot of cases where binding an event listener to the document or window objects is necessary for the functionality of the website to work. I have definitely run into this necessity in a couple of my little side projects.

I propose that a new event-target property (already discussed briefly with Matthew) is added to allow for this functionality. This should be a clean approach to the problem.

body {
  event: custom ${() => this.count++};
  event-target: ${window};
}

data-toggle

Similar to attr-toggle, removes the attribute when the value is false.

#app {
  data-value: theme dark;
  data-toggle: theme true;
}

#app:not(.dark-mode) {
  data-toggle: theme false;
}

Identifiers vs. strings

Corset treats identifiers and strings as interchangeable. The only difference is what characters are allowed; identifiers only allow [a-zA-Z] (note this is probably not to CSS spec and should be fixed).

The use case here is aesthetics and the belief that there wouldn't be builtin identifiers in Corset.

button {
  event: click var(--increment);
}

vs.

button {
  event: "click" var(--increment);
}

However, @propjockey provided a reason for why we might want builtin identifiers. This would be nice:

.profile:not(.editing) {
  text: initial;
}

I think there's a choice here of one of these:

  1. Force using strings where identifiers are used now.
    • Maybe this would be more intuitive anyways?
  2. Have reserved words like initial but otherwise allow identifiers as strings.
    • Hard to predict ahead of time what we need to reserve.
  3. Keep identifiers as strings and use functions for things like initial. initial().
    • Might be unexpected coming from CSS.
  4. Allow identifiers as strings only in certain properties. For example could we allow them as the event name, without allowing them generally?

[Parity] text property is currently adding commas between items

v0.8.10

text: "hello" var(--c) "world";

if --c isn't set, that results in "hello,,world" being inserted

in CSS, content would concatenate them directly without the comma joiner so "helloworld" would ideally be the result instead

It could be interesting to have a separate text-joiner property that specifies what the joiner string is though, defaulting to empty string

Discussion: replace bracket syntax used in property names

This issue is to discuss whether it would be possible and best for corset to avoid the bracket syntax used in CSS property names. The brackets would be avoided to follow the CSS Syntax rules for consuming a list of declarations, which limits property names to identifiers.

Current Syntax

::part(increment) {
  event[click]: var(--inc);
}

Possible Solutions

An event-set property to add a given event type with a given event listener.

::part(increment) {
  event-set: click var(--inc);
}

Questions:

  • Would it replace the event listener of an existing event type?
  • Or would it add a new event listener to the given event type?
  • How is this handled currently?

Change CDN urls

https://cdn.corset.dev/v/1.0.0/main.js. The v is so we can add r (for resolve) for semver support in the future.

Lists break when existing nodes are in the list container?

I've been playing around with Corset, and there's lots to like here! I'm very much on board with the project philosophy. 👍👍

One thing I'm wondering: it would be great to be able to server-render out items in a list, and have that still work with the client bindings (assuming the data matches). Right now (in my testing at least), it seems that if a list container isn't empty, the bindings don't work. I don't seem to get any particular error, it just seems broken. If I manually wipe out the container ahead of mounting, then of course it's fine.

Here's an example of what I mean based on one of the Corset examples: https://codepen.io/jaredcwhite/pen/LYmQdyW

Release 1.0

  • - Release 1.0
  • - Update website
  • - Update examples

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.