{"id":18383070,"url":"https://github.com/timkelty/craftcms-classmate","last_synced_at":"2025-04-06T23:32:06.401Z","repository":{"id":57069921,"uuid":"328860408","full_name":"timkelty/craftcms-classmate","owner":"timkelty","description":null,"archived":false,"fork":false,"pushed_at":"2021-01-28T19:29:46.000Z","size":191,"stargazers_count":8,"open_issues_count":3,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-22T09:03:54.478Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/timkelty.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-01-12T03:25:33.000Z","updated_at":"2021-06-01T15:21:52.000Z","dependencies_parsed_at":"2022-08-24T14:54:19.762Z","dependency_job_id":null,"html_url":"https://github.com/timkelty/craftcms-classmate","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timkelty%2Fcraftcms-classmate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timkelty%2Fcraftcms-classmate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timkelty%2Fcraftcms-classmate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timkelty%2Fcraftcms-classmate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/timkelty","download_url":"https://codeload.github.com/timkelty/craftcms-classmate/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247569124,"owners_count":20959758,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-06T01:09:57.789Z","updated_at":"2025-04-06T23:32:04.258Z","avatar_url":"https://github.com/timkelty.png","language":"PHP","funding_links":["https://www.paypal.com/donate?hosted_button_id=A5FPKHYV4GRTC"],"categories":[],"sub_categories":[],"readme":"# Classmate for Craft CMS 3\n\n[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/donate?hosted_button_id=A5FPKHYV4GRTC)\n\n![Test Status](https://github.com/timkelty/craftcms-classmate/workflows/Codeception/badge.svg)\n\nClassmate is here to help with HTML class composition and is especially useful when paired with a [utility-first](https://tailwindcss.com/docs/utility-first) css framework, such as [Tailwind CSS](http://tailwindcss.com/).\n\n### Before Classmate:\n\n`template.twig`\n\n```html\n\u003ch2 class=\"text-lg leading-6 font-medium text-gray-900 mb-3\"\u003e\n  Some reusable heading…\n  \u003ca href=\"#\" class=\"text-orange-600 hover:text-orange-900\"\u003eA link\u003c/a\u003e\n\u003c/h2\u003e\n```\n\n### After Classmate:\n\n`template.twig`\n\n```twig\n\u003ch2\n  class=\"{{ classmate.get('myHeading').remove('text-gray-900').add('mb-3', 'text-gray-400') }\"\n\u003e\n  Some reusable heading…\n  \u003ca href=\"#\" class=\"{{ classmate.get('defaultLink') }}\"\u003eA link\u003c/a\u003e\n\u003c/h2\u003e\n```\n\n`classmate.json`\n\n```json\n{\n  \"myHeading\": \"text-lg leading-6 font-medium text-gray-900\",\n  \"defaultLink\": \"text-orange-600 hover:text-orange-900\"\n}\n```\n\n## Why?\n\nMy opinons about [Extracting component classes with `@apply`](https://tailwindcss.com/docs/extracting-components#extracting-component-classes-with-apply) are pretty well summed up by [this tweet](https://twitter.com/adamwathan/status/1308944904786268161):\n\n![Flowchart: \"Should you extract a component class with @apply\"](https://pbs.twimg.com/media/EipNW97WsAIl8QD?format=jpg\u0026name=4096x4096)\n\nIn short, I tend to avoid `@apply` when possible.\n\n_Classmate was created specifically for single element components (e.g. button, link, heading), where extracting to a template partial may be cumbersome and you still want to avoid `@apply`._\n\n**How is this better than @apply? Aren't we just moving problem elsewhere?**\n\nYes and no. It is true that the practice of extracting components with `@apply` is similar to defining classes in classmate JSON file. However, Classmate abstracts the definition on the backend (PHP), while `@apply` abstracts it as part of the front-end build. Advantages to this are:\n\n- No additional CSS bloat from component classes\n- No added compile time from `@apply`\n- Your resulting HTML remains all-utility. Onboarding a new developer to project, especially if they're already familiar with Tailwind or whatever framework, is much easier if they don't have to decipher a set of component classes.\n- Errors are more easily caught.\n  - With an extracted component, misuse can happen easily and go unnoticed, e.g. a typo in your class attribute. With Classmate, when a class definition is missing, it is readily apparent to the developer.\n- Config can be shared with frontend JS.\n\n## Configuration\n\nCopy `./src/config.php` to `\u003cyourProject\u003e/config/classmate.php`.\n\n### `filePath`\n\nThe location of your Classmate file. Aliases and environment variables are supported.\nDefaults to `@config/classmate.json`, but I suggest you put it alongside your pre-compiled frontend assets, e.g. `./src/css/classmate.json`\n\nIf using Tailwind or PurgeCSS directly, you will also want to include this path. E.g.\n\n`tailwind.config.js`\n\n```js\nmodule.exports = {\n  purge: {\n    content: [\n      \"./src/**/*.css\",\n      \"./src/**/*.js\",\n      \"./templates/**/*.*\",\n      \"./config/tailwind.json\",\n    ],\n    options: {\n      safelist: [],\n    },\n  },\n};\n```\n\n## Classmate File\n\nYour Classmate file is a JSON file with a single object.\nThe values can be space separated strings or arrays, or a combination of both.\n\n```json\n{\n  \"heading1\": \"text-2xl font-bold\",\n  \"heading2\": [\"text-lg\", \"font-bold\"],\n  \"buttonBase\": [\n    \"text-center inline-flex items-center justify-center font-bold\",\n    \"rounded-full\"\n  ],\n  \"buttonWhite\": \"bg-white text-gray-900\",\n  \"buttonLg\": \"leading-none text-xl py-4 px-8\",\n  \"buttonSm\": \"leading-none text-sm py-2 px-4\",\n  \"centerX\": \"left-1/2 transform -translate-x-1/2\",\n  \"centerY\": \"top-1/2 transform -translate-y-1/2\"\n}\n```\n\nSince this is just a JSON file, it is easily consumable by Javascript, too!\n\n```js\nimport classmate from \"../../config/classmate.json\";\ndocument.querySelector(\"body\").class = classmate.body;\n```\n\n## Usage\n\n### `tag` function\n\n```twig\n{{ tag('a', classmate.get('defaultLink').asAttributes({\n  text: 'A link'\n  href: '#'\n})) }}\n```\n\n### `tag` tag\n\n_Craft 3.6+ only_\n\n```twig\n{% tag('a', classmate.get('defaultLink').asAttributes({href: '#' })) %}\n  A link\n{% endtag %}\n```\n\n### `attr` function\n\n```twig\n\u003ca {{ attr({\n  href: '#'\n  class: classmate.get('defaultLink').asClasses()\n}) }}\u003eA link\u003c/a\u003e\n```\n\n### `attr` filter\n\n```twig\n{% set tag = '\u003ca href=\"#\"\u003e' %}\n{{ tag|attr({\n    class: classmate.get('defaultLink').asClasses()\n}) }}\n```\n\n### Class string\n\n```twig\n\u003ca href=\"#\" class=\"{{ classmate.get('defaultLink') }}\"\u003eA link\u003c/a\u003e\n```\n\n## API\n\n`classmate` is a chainable API, available as a global in your Twig templates.\n\n### `get(string ...$keys): Classmate`\n\nRetrive classes of given `$keys` from your classmate file. Multiple keys will be merged right to left.\n\n```twig\n{{ classmate.get('buttonBase', 'buttonLarge', 'buttonBlue') }}\n```\n\n### `asClasses(): array`\n\nRetrive an array of classes. Duplicates and empty values are removed.\n\n```twig\n\u003cdiv {{ attr({\n  class: classmate.get('foo').asClasses()\n}) }}\"\u003e\n```\n\n### `asAttributes(iterable $attributes = []): array`\n\nRetreive an `attr`-compatible iterable, with `class` set, merged into any passed `$attributes`.\n\n```twig\n\u003cdiv {{ attr(classmate.get('foo').asAttributes({\n  id: 'buttonLarge'\n})) }}\"\u003e\n```\n\n### `add(string ...$classes): Classmate`\n\nAdd classes to the current `ClassList`.\n\n```twig\n{{ classmate.get('foo').add('mb-4') }}\n```\n\n### `remove(string ...$classes): Classmate`\n\nRemove classes to the current `ClassList`.\n\n```twig\n{{ classmate.get('foo').remove('mb-4') }}\n```\n\n### `matching(string $pattern): Classmate`\n\nFilter the current `ClassList`, keeping those that match `$pattern`.\n\n```twig\n{{ classmate.get('foo').matching('/^text-/') }}\n```\n\n### `notMatching(string $pattern): Classmate`\n\nFilter the current `ClassList`, removing those that match `$pattern`.\n\n```twig\n{{ classmate.get('foo').notMatching('/^mb-/') }}\n```\n\n### `replace(string $search, string $replace, bool $partial = false): Classmate`\n\nReplace `$search` with `$replace`. Set `$partial` to `true` to match partial strings, otherwise only complete matches will be replaced.\n\n```twig\n{{ classmate.get('foo').replace('text-red-500', 'text-red-100') }}\n{{ classmate.get('bar').replace('md:', 'lg:', true) }}\n```\n\n### `prepend(string $string): Classmate`\n\nPrepend `$string` to each item in the `ClassList`.\n\n```twig\n{{ classmate.get('foo').prepend('md:') }}\n```\n\n### `append(string $string): Classmate`\n\nAppend `$string` to each item in the `ClassList`.\n\n## Cache\n\nRetrival of the JSON file is cached, and invalidated by modifications to the file, so you really shouldn't have to worry much about invalidation. However, you can selectively clear the cache via the CP or with the CLI command:\n\n```bash\n./craft clear-caches/classmate-cache\n```\n\n## Requirements\n\n- Craft CMS 3.0+\n- PHP 7.4+\n\n## Installation\n\n```bash\ncomposer require timkelty/craftcms-classmate\n```\n\n## Roadmap\n\n### How do I use Classmate classes in css?\n\nYou can't! …yet.\n@markhuot alluded to this here: https://twitter.com/markhuot/status/1351605237736419329\n\nA Tailwind/Postcss plugin might allow something like:\n\nE.g. you write\n\n```css\n.prose a {\n  @classmate foo, bar;\n}\n```\n\n…to grab classes the same what that `classmate.get('foo', 'bar')` would.\n\n### Arrow Functions\n\nI'd love to allow the use of arrow functions, but Twig currently doesn't allow it.\nFor example, instead of our `matches` and `prepend` methods, I'd rather have:\n\n```twig\nclassmate.get('foo').filter(c =\u003e c starts with 'f')\nclassmate.get('foo').filter(c =\u003e c matches '/^f/')\nclassmate.get('foo').map(c =\u003e \"md:#{c}\")\n```\n\nWhile this works for Twig's `map`, `filter`, and `reduce` filters, it doesn't work here. See https://github.com/twigphp/Twig/issues/3402\n\n### CP settings\n\nI don't really have a desire for this, but happy to accept a PR if someone wants to add this.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftimkelty%2Fcraftcms-classmate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftimkelty%2Fcraftcms-classmate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftimkelty%2Fcraftcms-classmate/lists"}